tuwien.auto.calimero.link.AbstractLink Maven / Gradle / Ivy
Show all versions of calimero-core Show documentation
/*
Calimero 2 - A library for KNX network access
Copyright (c) 2015, 2016 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.link;
import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXAddress;
import tuwien.auto.calimero.Priority;
import tuwien.auto.calimero.cemi.CEMI;
import tuwien.auto.calimero.cemi.CEMIDevMgmt;
import tuwien.auto.calimero.cemi.CEMIFactory;
import tuwien.auto.calimero.cemi.CEMILData;
import tuwien.auto.calimero.cemi.CEMILDataEx;
import tuwien.auto.calimero.exception.KNXFormatException;
import tuwien.auto.calimero.exception.KNXIllegalArgumentException;
import tuwien.auto.calimero.exception.KNXTimeoutException;
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
import tuwien.auto.calimero.link.medium.PLSettings;
import tuwien.auto.calimero.link.medium.RFSettings;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.log.LogLevel;
import tuwien.auto.calimero.log.LogManager;
import tuwien.auto.calimero.log.LogService;
/**
* Provides an abstract KNX network link implementation, independent of the actual communication
* protocol and medium access. The link supports EMI1/EMI2/cEMI format. Subtypes extend this link by
* specifying the protocol, e.g., KNXnet/IP, or providing a specific implementation of medium
* access, e.g., via a serial port driver. In most cases, it is sufficient for a subtype to provide
* an implementation of {@link #onSend(CEMILData, boolean)} or
* {@link #onSend(KNXAddress, byte[], boolean)}, as well as {@link #onClose()}. If the communication
* protocol message format differs from the supported EMI 1/2 or cEMI, a subtype needs to override
* {@link #onReceive(FrameEvent)}. For receiving and dispatching frames from the specified protocol,
* a subtype uses the KNXListener {@link AbstractLink#notifier} as connection listener.
*
* In general, once a link has been closed, it is not available for further link communication and
* cannot be reopened.
*
* @author B. Malinowsky
* @see KNXNetworkLinkIP
* @see KNXNetworkLinkFT12
*/
public abstract class AbstractLink implements KNXNetworkLink
{
/** Logger for this link instance. */
protected final LogService logger;
/**
* Listener and notifier of KNX link events, add as connection listener to the underlying
* protocol during initialization.
*/
protected final EventNotifier notifier;
/** The message format to generate: cEMI or EMI1/EMI2. */
protected boolean cEMI = true;
/**
* With cEMI format, use {@link #onSend(KNXAddress, byte[], boolean)} if set true
,
* use {@link #onSend(CEMILData, boolean)} if set false
.
*/
protected boolean sendCEmiAsByteArray;
final Object conn;
private final String name;
private volatile boolean closed;
private volatile int hopCount = 6;
private KNXMediumSettings medium;
private final class LinkNotifier extends EventNotifier
{
LinkNotifier()
{
super(AbstractLink.this, AbstractLink.this.logger);
}
public void frameReceived(final FrameEvent e)
{
try {
final CEMI cemi = onReceive(e);
if (cemi instanceof CEMIDevMgmt) {
// XXX check .con correctly (required for setting cEMI link layer mode)
final int mc = cemi.getMessageCode();
if (mc == CEMIDevMgmt.MC_PROPWRITE_CON) {
final CEMIDevMgmt f = (CEMIDevMgmt) cemi;
if (f.isNegativeResponse())
logger.error("L-DM negative response, " + f.getErrorMessage());
}
}
// from this point on, we are only dealing with L_Data
if (!(cemi instanceof CEMILData))
return;
final CEMILData f = (CEMILData) cemi;
final int mc = f.getMessageCode();
if (mc == CEMILData.MC_LDATA_IND) {
addEvent(new Indication(new FrameEvent(source, f)));
logger.trace("indication from " + f.getSource());
}
else if (mc == CEMILData.MC_LDATA_CON) {
addEvent(new Confirmation(new FrameEvent(source, f)));
logger.trace("confirmation of " + f.getDestination());
}
else
logger.warn("unspecified frame event - ignored, msg code = 0x"
+ Integer.toHexString(mc));
}
catch (final KNXFormatException kfe) {
logger.warn("received unspecified frame " +
DataUnitBuilder.toHex(e.getFrameBytes(), ""), kfe);
}
catch (final RuntimeException rte) {
logger.warn("received unspecified frame " +
DataUnitBuilder.toHex(e.getFrameBytes(), ""), rte);
}
}
public void connectionClosed(final CloseEvent e)
{
((AbstractLink) source).closed = true;
super.connectionClosed(e);
logger.info("link closed");
LogManager.getManager().removeLogService(logger.getName());
}
};
/**
* @param connection the connection object
* @param name the link name
* @param settings medium settings of the accessed KNX network
*/
protected AbstractLink(final Object connection, final String name,
final KNXMediumSettings settings)
{
conn = connection;
this.name = name;
logger = LogManager.getManager().getLogService("calimero.link." + getName());
notifier = new LinkNotifier();
setKNXMedium(settings);
notifier.start();
}
/**
* This constructor does not start the event notifier.
*
* @param name the link name
* @param settings medium settings of the accessed KNX network
*/
protected AbstractLink(final String name, final KNXMediumSettings settings)
{
conn = null;
this.name = name;
logger = LogManager.getManager().getLogService("calimero.link." + getName());
notifier = new LinkNotifier();
setKNXMedium(settings);
}
public final synchronized void setKNXMedium(final KNXMediumSettings settings)
{
if (settings == null)
throw new KNXIllegalArgumentException("medium settings are mandatory");
if (medium != null && !settings.getClass().isAssignableFrom(medium.getClass())
&& !medium.getClass().isAssignableFrom(settings.getClass()))
throw new KNXIllegalArgumentException("medium differs");
medium = settings;
}
public final synchronized KNXMediumSettings getKNXMedium()
{
return medium;
}
public void addLinkListener(final NetworkLinkListener l)
{
notifier.addListener(l);
}
public void removeLinkListener(final NetworkLinkListener l)
{
notifier.removeListener(l);
}
public final void setHopCount(final int count)
{
if (count < 0 || count > 7)
throw new KNXIllegalArgumentException("hop count out of range [0..7]");
hopCount = count;
logger.info("hop count set to " + count);
}
public final int getHopCount()
{
return hopCount;
}
public void sendRequest(final KNXAddress dst, final Priority p, final byte[] nsdu)
throws KNXTimeoutException, KNXLinkClosedException
{
send(CEMILData.MC_LDATA_REQ, dst, p, nsdu, false);
}
public void sendRequestWait(final KNXAddress dst, final Priority p, final byte[] nsdu)
throws KNXTimeoutException, KNXLinkClosedException
{
send(CEMILData.MC_LDATA_REQ, dst, p, nsdu, true);
}
public void send(final CEMILData msg, final boolean waitForCon) throws KNXTimeoutException,
KNXLinkClosedException
{
if (closed)
throw new KNXLinkClosedException("link closed");
if (cEMI && !sendCEmiAsByteArray) {
final CEMILData adjusted = adjustMsgType(msg);
addMediumInfo(adjusted);
onSend(adjusted, waitForCon);
return;
}
onSend(msg.getDestination(), createEmi(msg), waitForCon);
}
public final String getName()
{
return name;
}
public final boolean isOpen()
{
return !closed;
}
public final void close()
{
synchronized (this) {
if (closed)
return;
closed = true;
}
onClose();
notifier.quit();
}
public String toString()
{
return "link" + (closed ? " (closed) " : " ") + getName() + " " + medium + ", hopcount " + hopCount;
}
/**
* Prepares the message in the required EMI format, using the supplied message parameters, and
* calls {@link #onSend(CEMILData, boolean)} and {@link #onSend(KNXAddress, byte[], boolean)}.
*
* @param mc message code
* @param dst KNX destination address
* @param p message priority
* @param nsdu NSDU
* @param waitForCon true
to wait for a link layer confirmation response,
* false
to not wait for the confirmation
* @throws KNXTimeoutException on a timeout during send or while waiting for the confirmation
* @throws KNXLinkClosedException if the link is closed
*/
protected void send(final int mc, final KNXAddress dst, final Priority p, final byte[] nsdu,
final boolean waitForCon) throws KNXTimeoutException, KNXLinkClosedException
{
if (closed)
throw new KNXLinkClosedException("link closed");
if (cEMI && !sendCEmiAsByteArray) {
final CEMILData f = cEMI(mc, dst, p, nsdu);
onSend(f, waitForCon);
return;
}
onSend(dst, createEmi(mc, dst, p, nsdu), waitForCon);
}
/**
* Implement with the connection/medium-specific protocol send primitive. Sends the message
* supplied as byte array over the link. In case a L-Data confirmation is requested, the message
* send is only successful if the corresponding confirmation is received.
*
* @param dst for logging purposes only: the KNX destination address, or null
* @param msg the message to send
* @param waitForCon true
to wait for a link layer confirmation response,
* false
to not wait for the confirmation
* @throws KNXTimeoutException on a timeout during send or while waiting for the confirmation
* @throws KNXLinkClosedException if the link is closed
*/
protected abstract void onSend(final KNXAddress dst, final byte[] msg, final boolean waitForCon)
throws KNXTimeoutException, KNXLinkClosedException;
/**
* Implement with the connection/medium-specific protocol send primitive. Sends the message
* supplied in cEMI L-Data format over the link. In case a L-Data confirmation is requested, the
* message send is only successful if the corresponding confirmation is received.
*
* @param msg the message to send
* @param waitForCon true
, if the communication protocol should block and wait for
* the link layer confirmation (L-Data.con), executing all required retransmission
* attempts and timeout validations, false
if no confirmation is requested
* @throws KNXTimeoutException on a timeout during send or while waiting for the confirmation
* @throws KNXLinkClosedException if the link is closed
*/
protected abstract void onSend(CEMILData msg, boolean waitForCon)
throws KNXTimeoutException, KNXLinkClosedException;
/**
* Returns a cEMI representation, e.g., cEMI L-Data, using the received frame event for EMI and
* cEMI formats. Override this method if the used message format does not conform to the
* supported EMI 1/2 or cEMI format.
*
* @param e the received frame event
* @return the constructed cEMI message, e.g., cEMI L-Data
* @throws KNXFormatException on unsupported frame formats, or errors in the frame format
*/
protected CEMI onReceive(final FrameEvent e) throws KNXFormatException
{
final CEMI f = e.getFrame();
return f != null ? f : CEMIFactory.createFromEMI(e.getFrameBytes());
}
/**
* Invoked on {@link #close()} to execute additional close sequences of the communication
* protocol, or releasing link-specific resources.
*/
protected void onClose() {}
private CEMILData adjustMsgType(final CEMILData msg)
{
final boolean srcOk = msg.getSource().getRawAddress() != 0;
// just return if we don't need to adjust source address and don't need LDataEx
if ((srcOk || medium.getDeviceAddress().getRawAddress() == 0)
&& (medium instanceof TPSettings || msg instanceof CEMILDataEx))
return msg;
return CEMIFactory.create(srcOk ? null : medium.getDeviceAddress(), null, msg, true);
}
private void addMediumInfo(final CEMILData msg)
{
String s = "";
if (medium instanceof PLSettings) {
final CEMILDataEx f = (CEMILDataEx) msg;
if (f.getAdditionalInfo(CEMILDataEx.ADDINFO_PLMEDIUM) == null)
f.addAdditionalInfo(CEMILDataEx.ADDINFO_PLMEDIUM,
((PLSettings) medium).getDomainAddress());
}
else if (medium.getMedium() == KNXMediumSettings.MEDIUM_RF) {
final CEMILDataEx f = (CEMILDataEx) msg;
if (f.getAdditionalInfo(CEMILDataEx.ADDINFO_RFMEDIUM) == null) {
final RFSettings rf = (RFSettings) medium;
final byte[] sn = f.isDomainBroadcast() ? rf.getDomainAddress() : rf
.getSerialNumber();
// add-info: rf-info (0 ignores a lot), sn (6 bytes), lfn (=255:void)
final byte[] ai = new byte[] { 0, sn[0], sn[1], sn[2], sn[3], sn[4], sn[5],
(byte) 0xff };
f.addAdditionalInfo(CEMILDataEx.ADDINFO_RFMEDIUM, ai);
s = f.isDomainBroadcast() ? "(using domain address)" : "(using device SN)";
}
}
else
return;
if (logger.isLoggable(LogLevel.TRACE))
logger.trace("add cEMI additional info for " + medium.getMediumString() + " " + s);
}
// Creates the target EMI format using L-Data message parameters
private byte[] createEmi(final int mc, final KNXAddress dst, final Priority p,
final byte[] nsdu)
{
if (cEMI)
return cEMI(mc, dst, p, nsdu).toByteArray();
final boolean repeat = true;
final boolean ackRequest = false;
final boolean posCon = true;
return CEMIFactory.toEmi(mc, dst, p, repeat, ackRequest, posCon, hopCount, nsdu);
}
// Creates the target EMI format using a cEMI L-Data message
private byte[] createEmi(final CEMILData f)
{
if (cEMI) {
final CEMILData adjusted = adjustMsgType(f);
addMediumInfo(adjusted);
return adjusted.toByteArray();
}
return CEMIFactory.toEmi(f);
}
private CEMILData cEMI(final int mc, final KNXAddress dst, final Priority p, final byte[] nsdu)
{
final IndividualAddress src = medium.getDeviceAddress();
// use default address 0 in system broadcast
final KNXAddress d = dst == null ? new GroupAddress(0) : dst;
final boolean tp = medium.getMedium() == KNXMediumSettings.MEDIUM_TP1;
if (nsdu.length <= 16 && tp)
return new CEMILData(mc, src, d, nsdu, p, true, hopCount);
final boolean pl = medium.getMedium() == KNXMediumSettings.MEDIUM_PL110;
// TODO allow domain bcast for RF, currently we send all RF L_Data as system broadcast
final boolean domainBcast = tp ? true : pl && dst != null ? true : false;
final CEMILDataEx f = new CEMILDataEx(mc, src, d, nsdu, p, true, domainBcast, false,
hopCount);
addMediumInfo(f);
return f;
}
}