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

tuwien.auto.calimero.knxnetip.ConnectionBase Maven / Gradle / Ivy

The newest version!
/*
    Calimero 2 - A library for KNX network access
    Copyright (c) 2010, 2024 B. Malinowsky

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Linking this library statically or dynamically with other modules is
    making a combined work based on this library. Thus, the terms and
    conditions of the GNU General Public License cover the whole
    combination.

    As a special exception, the copyright holders of this library give you
    permission to link this library with independent modules to produce an
    executable, regardless of the license terms of these independent
    modules, and to copy and distribute the resulting executable under terms
    of your choice, provided that you also meet, for each linked independent
    module, the terms and conditions of the license of that module. An
    independent module is a module which is not derived from or based on
    this library. If you modify this library, you may extend this exception
    to your version of the library, but you are not obligated to do so. If
    you do not wish to do so, delete this exception statement from your
    version.
*/

package tuwien.auto.calimero.knxnetip;

import static tuwien.auto.calimero.knxnetip.KNXnetIPConnection.BlockingMode.NonBlocking;
import static tuwien.auto.calimero.knxnetip.KNXnetIPConnection.BlockingMode.WaitForAck;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;

import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.KNXAckTimeoutException;
import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.KNXListener;
import tuwien.auto.calimero.KNXTimeoutException;
import tuwien.auto.calimero.ServiceType;
import tuwien.auto.calimero.cemi.CEMI;
import tuwien.auto.calimero.internal.EventListeners;
import tuwien.auto.calimero.internal.Executor;
import tuwien.auto.calimero.knxnetip.servicetype.DisconnectRequest;
import tuwien.auto.calimero.knxnetip.servicetype.ErrorCodes;
import tuwien.auto.calimero.knxnetip.servicetype.KNXnetIPHeader;
import tuwien.auto.calimero.knxnetip.servicetype.PacketHelper;
import tuwien.auto.calimero.knxnetip.servicetype.RoutingIndication;
import tuwien.auto.calimero.knxnetip.servicetype.ServiceRequest;
import tuwien.auto.calimero.knxnetip.util.HPAI;
import tuwien.auto.calimero.log.LogService.LogLevel;

/**
 * Generic implementation of a KNXnet/IP connection, used for tunneling, device management and routing.
 *
 * @author B. Malinowsky
 */
public abstract class ConnectionBase implements KNXnetIPConnection
{
	/**
	 * Status code of communication: waiting for service acknowledgment after send, no error, not ready to send.
	 */
	public static final int ACK_PENDING = 2;

	/**
	 * Status code of communication: in idle state, received a service acknowledgment error as response, ready to send.
	 */
	public static final int ACK_ERROR = 3;

	// KNXnet/IP client SHALL wait 10 seconds for a connect response frame from server
	static final int CONNECT_REQ_TIMEOUT = 10;

	/** Local control endpoint socket, only assigned and valid if UDP is used. */
	protected DatagramSocket ctrlSocket;
	/** Local data endpoint socket, only assigned and valid if UDP is used. */
	protected DatagramSocket socket;

	/** Remote control endpoint. */
	protected InetSocketAddress ctrlEndpt;
	/** Remote data endpoint. */
	protected InetSocketAddress dataEndpt;

	/** Connection KNX channel identifier. */
	protected int channelId;
	/** Use network address translation (NAT) aware communication. */
	protected boolean useNat;

	/** Service request code used for this connection type. */
	protected final int serviceRequest;
	/** Acknowledgment service type used for this connection type. */
	protected final int serviceAck;
	/** Container for event listeners. */
	protected final EventListeners listeners = new EventListeners<>();
	/** Logger for this connection. */
	protected Logger logger;

	// current state visible to the user
	// a state < 0 indicates severe error state
	volatile int state = CLOSED;
	/** Internal state, so we can use states not visible to the user. */
	protected volatile int internalState = CLOSED;
	// should an internal state change update the state member
	volatile boolean updateState = true;

	// flag to ensure close is invoked only once
	volatile int closing;

	// number of maximum sends (= retries + 1)
	final int maxSendAttempts;
	// timeout for response message in seconds
	final int responseTimeout;

	CEMI keepForCon;

	private ReceiverLoop receiver;

	// lock object to do wait() on for protocol timeouts
	final ReentrantLock lock = new ReentrantLock(true);
	private final Condition cond = lock.newCondition();

	// send/receive sequence numbers
	private int seqRcv;
	private int seqSend;

	private final Semaphore sendWaitQueue = new Semaphore();
	private boolean inBlockingSend;

	/**
	 * Base constructor to assign the supplied arguments.
	 *
	 * @param serviceRequest service request code of the protocol
	 * @param serviceAck service ack code of the protocol
	 * @param maxSendAttempts maximum send attempts of a datagram
	 * @param responseTimeout response timeout in seconds
	 */
	protected ConnectionBase(final int serviceRequest, final int serviceAck, final int maxSendAttempts,
		final int responseTimeout)
	{
		this.serviceRequest = serviceRequest;
		this.serviceAck = serviceAck;
		this.maxSendAttempts = maxSendAttempts;
		this.responseTimeout = responseTimeout;
	}

	@Override
	public void addConnectionListener(final KNXListener l)
	{
		listeners.add(l);
	}

	@Override
	public void removeConnectionListener(final KNXListener l)
	{
		listeners.remove(l);
	}

	/**
	 * If {@code mode} is {@link BlockingMode#WaitForCon} or {@link BlockingMode#WaitForAck}, the sequence
	 * order of more {@link #send(CEMI, BlockingMode)} calls from different threads is being maintained according to
	 * invocation order (FIFO).
* Calling send blocks until any previous invocation finished, then communication proceeds according to the protocol * and waits for response (either ACK or cEMI confirmation), timeout, or an error condition.
* Note that, for now, when using blocking mode any ongoing nonblocking invocation is not detected or considered for * waiting until completion. *

* If mode is {@link BlockingMode#NonBlocking}, sending is only permitted if no other send is currently being done, * otherwise {@link IllegalStateException} is thrown. In this mode, a user has to check communication state on * its own ({@link #getState()}). */ @Override public void send(final CEMI frame, final BlockingMode mode) throws KNXTimeoutException, KNXConnectionClosedException, InterruptedException { // send state | blocking | nonblocking // ----------------------------------- // OK |send+wait | send+return // WAIT |wait+s.+w.| throw // ACK_ERROR |send+wait | send+return // ERROR |throw | throw if (state == CLOSED) { throw new KNXConnectionClosedException("send attempt on closed connection"); } if (state < 0) { logger.error("send invoked in error state " + state + " - aborted"); throw new IllegalStateException("in error state, send aborted"); } // arrange into line depending on blocking mode sendWaitQueue.acquire(mode != NonBlocking); lock.lock(); try { if (mode == NonBlocking && state != OK && state != ACK_ERROR) { logger.warn( "nonblocking send invoked while waiting for data response in state " + state + " - aborted"); sendWaitQueue.release(false); throw new IllegalStateException("waiting for data response"); } try { if (state == CLOSED) { throw new KNXConnectionClosedException("send attempt on closed connection"); } updateState = mode == NonBlocking; inBlockingSend = mode != NonBlocking; final byte[] buf; if (serviceRequest == KNXnetIPHeader.ROUTING_IND) buf = PacketHelper.toPacket(new RoutingIndication(frame)); else buf = PacketHelper.toPacket(new ServiceRequest<>(serviceRequest, channelId, getSeqSend(), frame)); keepForCon = frame; int attempt = 0; for (; attempt < maxSendAttempts; ++attempt) { if (logger.isTraceEnabled()) logger.trace("sending cEMI frame seq {}, {}, attempt {} (channel {}) {}", getSeqSend(), mode, (attempt + 1), channelId, DataUnitBuilder.toHex(buf, " ")); send(buf, dataEndpt); // shortcut for routing, don't switch into 'ack-pending' if (serviceRequest == KNXnetIPHeader.ROUTING_IND) return; // skip ack transition if we're using a tcp socket if (socket == null) { internalState = ClientConnection.CEMI_CON_PENDING; break; } internalState = ACK_PENDING; // always forward this state to user state = ACK_PENDING; if (mode == NonBlocking) return; waitForStateChange(ACK_PENDING, responseTimeout); if (internalState == ClientConnection.CEMI_CON_PENDING || internalState == OK) break; if (internalState == CLOSED) throw new KNXConnectionClosedException("waiting for service ack"); } // close connection on no service ack from server if (attempt == maxSendAttempts) { final KNXAckTimeoutException e = new KNXAckTimeoutException( "maximum send attempts, no service acknowledgment received"); close(CloseEvent.INTERNAL, "maximum send attempts", LogLevel.ERROR, e); throw e; } // always forward this state to user state = internalState; if (mode != WaitForAck) doExtraBlockingModes(); } catch (final InterruptedIOException e) { throw new InterruptedException("interrupted I/O, " + e); } catch (final IOException e) { close(CloseEvent.INTERNAL, "communication failure", LogLevel.ERROR, e); throw new KNXConnectionClosedException("connection closed", e); } finally { updateState = true; setState(internalState); inBlockingSend = false; // with routing we immediately release with any blocking mode, because there is no ack/.con if (mode != NonBlocking || serviceRequest == KNXnetIPHeader.ROUTING_IND) sendWaitQueue.release(mode != NonBlocking); } } finally { lock.unlock(); } } protected void send(final byte[] packet, final InetSocketAddress dst) throws IOException { final DatagramPacket p = new DatagramPacket(packet, packet.length, dst); if (dst.equals(dataEndpt)) socket.send(p); else ctrlSocket.send(p); } @Override public final InetSocketAddress getRemoteAddress() { if (state == CLOSED) return new InetSocketAddress(0); return ctrlEndpt; } @Override public final int getState() { return state; } @Override public String name() { // only the control endpoint is set when our logger is initialized (the data // endpoint gets assigned later in connect) // to keep the name short, avoid a prepended host name as done by InetAddress return Net.hostPort(ctrlEndpt); } @Override public final void close() { close(CloseEvent.USER_REQUEST, "user request", LogLevel.DEBUG, null); } @Override public String toString() { return name() + (channelId != 0 ? (" channel " + channelId) : "") + " (state " + connectionState() + ")"; } /** * Returns the protocol's current receive sequence number. * * @return receive sequence number as int */ protected synchronized int getSeqRcv() { return seqRcv; } /** * Increments the protocol's receive sequence number, with increment on sequence number 255 resulting in 0. */ protected synchronized void incSeqRcv() { seqRcv = (seqRcv + 1) & 0xFF; } /** * Returns the protocol's current send sequence number. * * @return send sequence number as int */ protected synchronized int getSeqSend() { return seqSend; } /** * Increments the protocol's send sequence number, with increment on sequence number 255 resulting in 0. */ protected synchronized void incSeqSend() { seqSend = (seqSend + 1) & 0xFF; } /** * Fires a frame received event ({@link KNXListener#frameReceived(FrameEvent)}) for the supplied cEMI * {@code frame}. * * @param frame the cEMI to generate the event for */ protected void fireFrameReceived(final CEMI frame) { final FrameEvent fe = new FrameEvent(this, frame); listeners.fire(l -> l.frameReceived(fe)); } boolean handleServiceType(final KNXnetIPHeader h, final byte[] data, final int offset, final InetSocketAddress source) throws KNXFormatException, IOException { final int hdrStart = offset - h.getStructLength(); logger.trace("from {}: {}: {}", Net.hostPort(source), h, DataUnitBuilder.toHex(Arrays.copyOfRange(data, hdrStart, hdrStart + h.getTotalLength()), " ")); return handleServiceType(h, data, offset, source.getAddress(), source.getPort()); } /** * This stub always returns false. * * @param h received KNXnet/IP header * @param data received datagram data * @param offset datagram data start offset * @param src sender IP address * @param port sender UDP port * @return {@code true} if service type was known and handled (successfully or not), {@code false} * otherwise * @throws KNXFormatException on service type parsing or data format errors * @throws IOException on socket problems */ @SuppressWarnings("unused") protected boolean handleServiceType(final KNXnetIPHeader h, final byte[] data, final int offset, final InetAddress src, final int port) throws KNXFormatException, IOException { // at this subtype level, we don't care about any service type return false; } /** * Request to set this connection into a new connection state. * * @param newState new state to set */ protected final void setState(final int newState) { if (closing < 2) { // detect and ignore order of arrival inversion for tunneling.ack/cEMI.con if (internalState == OK && newState == ClientConnection.CEMI_CON_PENDING) return; internalState = newState; if (updateState) state = newState; } else state = internalState = CLOSED; } /** * See {@link #setState(int)}, with additional notification of internal threads. * * @param newState new state to set */ protected final void setStateNotify(final int newState) { lock.lock(); try { setState(newState); if (newState == OK && !inBlockingSend) this.sendWaitQueue.release(false); // worst case: we notify 2 threads, the closing one and 1 sending cond.signalAll(); } finally { lock.unlock(); } } /** * Called on {@link #close()}, containing all protocol specific actions to close a connection. Before this method * returns, {@link #cleanup} is called. * * @param initiator one of the constants of {@link CloseEvent} * @param reason short text statement why close was called on this connection * @param level log level to use for logging, adjust this to the reason of closing this connection * @param t a throwable, to pass to the logger if the close event was caused by some error, can be {@code null} */ protected void close(final int initiator, final String reason, final LogLevel level, final Throwable t) { synchronized (this) { if (closing > 0) return; closing = 1; } lock.lock(); try { final boolean tcp = ctrlSocket == null; final var hpai = tcp ? HPAI.Tcp : useNat ? HPAI.Nat : new HPAI(HPAI.IPV4_UDP, (InetSocketAddress) ctrlSocket.getLocalSocketAddress()); logger.trace("sending disconnect request for {}", this); final byte[] buf = PacketHelper.toPacket(new DisconnectRequest(channelId, hpai)); send(buf, ctrlEndpt); long remaining = CONNECT_REQ_TIMEOUT * 1000L; final long end = System.currentTimeMillis() + remaining; while (closing == 1 && remaining > 0) { cond.await(remaining, TimeUnit.MILLISECONDS); remaining = end - System.currentTimeMillis(); } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } catch (IOException | RuntimeException e) { // we have to also catch RTEs here, since if socket already failed // before close(), getLocalSocketAddress() might throw illegal argument // exception or return the wildcard address, indicating a messed up socket logger.error("send disconnect failed", e); } finally { lock.unlock(); } cleanup(initiator, reason, level, t); } /** * @param initiator one of the constants of {@link CloseEvent} * @param reason short text statement why close was called on this connection * @param level log level to use for logging, adjust this to the reason of closing this connection * @param t a throwable, to pass to the logger if the close event was caused by some error, can be {@code null} */ protected void cleanup(final int initiator, final String reason, final LogLevel level, final Throwable t) { setStateNotify(CLOSED); fireConnectionClosed(initiator, reason); listeners.removeAll(); } protected boolean supportedVersion(final KNXnetIPHeader h) { final boolean supported = h.getVersion() == KNXnetIPConnection.KNXNETIP_VERSION_10; if (!supported) logger.warn("KNXnet/IP {}.{} {}", h.getVersion() >> 4, h.getVersion() & 0xf, ErrorCodes.getErrorMessage(ErrorCodes.VERSION_NOT_SUPPORTED)); return supported; } /** * Validates channel id received in a packet against the one assigned to this connection. * * @param id received id to check * @param svcType packet service type * @return {@code true} if valid, {@code false} otherwise */ protected boolean checkChannelId(final int id, final String svcType) { if (id == channelId) return true; logger.warn("received service " + svcType + " with wrong channel ID " + id + ", expected " + channelId + " - ignored"); return false; } /** * @deprecated No replacement. Use {@link ServiceRequest#from(KNXnetIPHeader, byte[], int)}. */ @Deprecated protected ServiceRequest getServiceRequest(final KNXnetIPHeader h, final byte[] data, final int offset) throws KNXFormatException { return ServiceRequest.from(h, data, offset); } final void startReceiver() { if (receiver == null) { final ReceiverLoop looper = new ReceiverLoop(this, socket, 0x200); Executor.execute(looper, "KNXnet/IP receiver"); receiver = looper; } } final void stopReceiver() { if (receiver != null) receiver.quit(); } boolean waitForStateChange(final int initialState, final int timeout) throws InterruptedException { boolean changed = false; long remaining = timeout * 1000L; final long end = System.currentTimeMillis() + remaining; lock.lock(); try { while (internalState == initialState && remaining > 0) { cond.await(remaining, TimeUnit.MILLISECONDS); remaining = end - System.currentTimeMillis(); } } finally { lock.unlock(); } changed = remaining > 0; return changed; } /** * Give chance to perform additional blocking modes called if mode is set to a blocking mode not equal to * NonBlocking and WaitForAck. This method is called from send() after WaitForAck was completed. * * @throws KNXTimeoutException * @throws InterruptedException on interrupted thread */ @SuppressWarnings("unused") void doExtraBlockingModes() throws KNXTimeoutException, InterruptedException {} String connectionState() { return switch (state) { case OK -> "OK"; case CLOSED -> "closed"; case ACK_PENDING -> "ACK pending"; case ACK_ERROR -> "ACK error"; default -> "unknown"; }; } private void fireConnectionClosed(final int initiator, final String reason) { final CloseEvent ce = new CloseEvent(this, initiator, reason); listeners.fire(l -> l.connectionClosed(ce)); } // a semaphore with fair use behavior (FIFO) // acquire and its associated release don't have to be invoked by same thread private static final class Semaphore { private static final class Node { Node next; boolean blocked; Node(final Node n) { next = n; blocked = true; } } private Node head; private Node tail; private int cnt; private int nonblockingCnt; Semaphore() { cnt = 1; nonblockingCnt = 0; } void acquire(final boolean blocking) { final Node n; boolean interrupted = false; synchronized (this) { if (cnt > 0 && tail == null) { --cnt; if (!blocking) nonblockingCnt++; return; } if (!blocking) { nonblockingCnt++; return; } n = enqueue(); } synchronized (n) { while (n.blocked) try { n.wait(); } catch (final InterruptedException e) { interrupted = true; } } synchronized (this) { dequeue(); --cnt; } if (interrupted) Thread.currentThread().interrupt(); } synchronized void release(final boolean blocking) { if (blocking) { if (++cnt > 0) notifyNext(); } else if (nonblockingCnt > 0) { nonblockingCnt--; if (nonblockingCnt == 0) { if (++cnt > 0) notifyNext(); } } } private Node enqueue() { final Node n = new Node(null); if (tail == null) tail = n; else head.next = n; head = n; return head; } private void notifyNext() { if (tail != null) synchronized (tail) { tail.blocked = false; tail.notify(); } } private void dequeue() { tail = tail.next; if (tail == null) head = null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy