tuwien.auto.calimero.mgmt.ManagementClientImpl Maven / Gradle / Ivy
Show all versions of calimero-core Show documentation
/*
Calimero 2 - A library for KNX network access
Copyright (c) 2006, 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.mgmt;
import static java.lang.String.format;
import static java.nio.ByteBuffer.allocate;
import static java.util.stream.Collectors.toList;
import static tuwien.auto.calimero.DataUnitBuilder.createAPDU;
import static tuwien.auto.calimero.DataUnitBuilder.createLengthOptimizedAPDU;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import org.slf4j.Logger;
import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.KNXInvalidResponseException;
import tuwien.auto.calimero.KNXRemoteException;
import tuwien.auto.calimero.KNXTimeoutException;
import tuwien.auto.calimero.Priority;
import tuwien.auto.calimero.ReturnCode;
import tuwien.auto.calimero.SerialNumber;
import tuwien.auto.calimero.cemi.CEMI;
import tuwien.auto.calimero.cemi.CEMILData;
import tuwien.auto.calimero.dptxlator.PropertyTypes;
import tuwien.auto.calimero.internal.EventListeners;
import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.log.LogService;
import tuwien.auto.calimero.mgmt.PropertyAccess.PID;
import tuwien.auto.calimero.secure.KnxSecureException;
import tuwien.auto.calimero.secure.Security;
import tuwien.auto.calimero.secure.SecurityControl;
import tuwien.auto.calimero.secure.SecurityControl.DataSecurity;
/**
* Implementation of the management client.
*
* Uses {@link TransportLayer} internally for communication, and {@link SecureManagement} for KNX Secure if required.
* All management service methods invoked after a detach of the network link are allowed
* to throw {@link IllegalStateException}.
*
* @author B. Malinowsky
*/
public class ManagementClientImpl implements ManagementClient
{
private static final int ADC_READ = 0x0180;
private static final int ADC_RESPONSE = 0x01C0;
private static final int AUTHORIZE_READ = 0x03D1;
private static final int AUTHORIZE_RESPONSE = 0x03D2;
private static final int DOA_WRITE = 0x3E0;
private static final int DOA_READ = 0x3E1;
private static final int DOA_RESPONSE = 0x3E2;
private static final int DOA_SELECTIVE_READ = 0x3E3;
private static final int IND_ADDR_READ = 0x0100;
private static final int IND_ADDR_RESPONSE = 0x0140;
private static final int IND_ADDR_WRITE = 0xC0;
private static final int IND_ADDR_SN_READ = 0x03DC;
private static final int IND_ADDR_SN_RESPONSE = 0x03DD;
private static final int IND_ADDR_SN_WRITE = 0x03DE;
private static final int DEVICE_DESC_READ = 0x300;
private static final int DEVICE_DESC_RESPONSE = 0x340;
private static final int KEY_WRITE = 0x03D3;
private static final int KEY_RESPONSE = 0x03D4;
private static final int MEMORY_READ = 0x0200;
private static final int MEMORY_RESPONSE = 0x0240;
private static final int MEMORY_WRITE = 0x0280;
private static final int PROPERTY_DESC_READ = 0x03D8;
private static final int PROPERTY_DESC_RESPONSE = 0x03D9;
private static final int PROPERTY_READ = 0x03D5;
private static final int PROPERTY_RESPONSE = 0x03D6;
private static final int PROPERTY_WRITE = 0x03D7;
private static final int FunctionPropertyExtCommand = 0b0111010100;
private static final int FunctionPropertyExtStateRead = 0b0111010101;
private static final int FunctionPropertyExtStateResponse = 0b0111010110;
private static final int SystemNetworkParamRead = 0b0111001000;
private static final int SystemNetworkParamResponse = 0b0111001001;
private static final int SystemNetworkParamWrite = 0b0111001010;
private static final int NetworkParamRead = 0b1111011010;
private static final int NetworkParamResponse = 0b1111011011;
static final int NetworkParamWrite = 0b1111100100;
private static final int MemoryExtendedWrite = 0b0111111011;
private static final int MemoryExtendedWriteResponse = 0b0111111100;
private static final int MemoryExtendedRead = 0b0111111101;
private static final int MemoryExtendedReadResponse = 0b0111111110;
private static final int DomainAddressSerialNumberWrite = 0b1111101110;
// serves as both req and res
private static final int RESTART = 0x0380;
private class TLListener implements TransportListener
{
TLListener() {}
@Override
public void broadcast(final FrameEvent e)
{
checkResponse(e);
}
@Override
public void dataConnected(final FrameEvent e)
{
checkResponse(e);
}
@Override
public void dataIndividual(final FrameEvent e)
{
checkResponse(e);
}
@Override
public void disconnected(final Destination d) {}
@Override
public void group(final FrameEvent e) {}
@Override
public void detached(final DetachEvent e) {}
@Override
public void linkClosed(final CloseEvent e)
{
logger.debug("attached link was closed");
}
private void checkResponse(final FrameEvent e) {
try {
if (isActiveService(e)) {
synchronized (indications) {
indications.add(e);
indications.notifyAll();
}
}
listeners.fire(c -> c.accept(e));
}
catch (final RuntimeException rte) {
final var cemi = e.getFrame();
final var src = cemi instanceof CEMILData ? ((CEMILData) cemi).getDestination() : "cEMI server";
logger.warn("on indication from {}", src, rte);
}
}
}
private static final boolean extMemoryServices = true;
private final TransportLayer tl;
private final TLListener tlListener = new TLListener();
private final SecureManagement sal;
private final boolean toolAccess = true;
private final IndividualAddress src;
private volatile Priority priority = Priority.LOW;
private volatile Duration responseTimeout = Duration.ofSeconds(5);
private final Deque indications = new ArrayDeque<>();
private final ConcurrentHashMap activeServiceResponses = new ConcurrentHashMap<>();
private volatile boolean detachTransportLayer;
private volatile boolean detached;
private final Logger logger;
private final EventListeners> listeners = new EventListeners<>();
SecureManagement secureApplicationLayer() { return sal; }
/**
* Creates a new management client attached to the supplied KNX network link.
* For secure management, {@link Security#defaultInstallation} is used.
*
* @param link network link used for communication with a KNX network, the client does not take ownership
* @throws KNXLinkClosedException if the network link is closed
*/
public ManagementClientImpl(final KNXNetworkLink link) throws KNXLinkClosedException
{
this(link, new TransportLayerImpl(link));
detachTransportLayer = true;
}
/**
* Creates a new management client attached to the supplied KNX network link, using {@code security} for secure
* management if required.
*
* @param link network link used for communication with a KNX network, the client does not take ownership
* @param security security with device tool keys to use for secure management
* @throws KNXLinkClosedException if the network link is closed
*/
public ManagementClientImpl(final KNXNetworkLink link, final Security security) throws KNXLinkClosedException {
this(link, new SecureManagement(new TransportLayerImpl(link), security.deviceToolKeys()));
detachTransportLayer = true;
}
/**
* Creates a new management client attached to the supplied KNX network link, and using the supplied transport layer instance.
*
* @param link network link used for communication with a KNX network, the client does not take ownership
* @param transportLayer transport layer, the client does not take ownership (and won't {@link TransportLayer#detach}
* the link)
*/
protected ManagementClientImpl(final KNXNetworkLink link, final TransportLayer transportLayer)
{
this(link, new SecureManagement(transportLayer, Security.defaultInstallation().deviceToolKeys()));
}
protected ManagementClientImpl(final KNXNetworkLink link, final SecureManagement secureManagement) {
tl = secureManagement.transportLayer();
logger = LogService.getLogger("calimero.mgmt.MC " + link.getName());
src = link.getKNXMedium().getDeviceAddress();
sal = secureManagement;
sal.addListener(tlListener);
}
/**
* Internal API.
*
* @param onEvent consumer to receive notifications about frame events
*/
public final void addEventListener(final Consumer onEvent)
{
listeners.add(onEvent);
}
/**
* Internal API.
*
* @param onEvent consumer to receive notifications about frame events
*/
public final void removeEventListener(final Consumer onEvent)
{
listeners.remove(onEvent);
}
@Override
public Duration responseTimeout() {
return responseTimeout;
}
@Override
public void responseTimeout(final Duration timeout) {
if (timeout.isNegative() || timeout.isZero())
throw new KNXIllegalArgumentException("timeout <= 0");
responseTimeout = timeout;
}
@Override
public void setPriority(final Priority p)
{
priority = p;
}
@Override
public Priority getPriority()
{
return priority;
}
@Override
public Destination createDestination(final IndividualAddress remote,
final boolean connectionOriented, final boolean keepAlive, final boolean verifyMode)
{
return tl.createDestination(remote, connectionOriented, keepAlive, verifyMode);
}
@Override
public void writeAddress(final IndividualAddress newAddress) throws KNXTimeoutException,
KNXLinkClosedException
{
tl.broadcast(false, Priority.SYSTEM, createAPDU(IND_ADDR_WRITE, newAddress.toByteArray()));
}
@Override
public IndividualAddress[] readAddress(final boolean oneAddressOnly)
throws KNXTimeoutException, KNXRemoteException, KNXLinkClosedException, InterruptedException
{
final long start = registerActiveService(IND_ADDR_RESPONSE);
tl.broadcast(false, Priority.SYSTEM, DataUnitBuilder.createLengthOptimizedAPDU(IND_ADDR_READ));
final List l = new ArrayList<>();
waitForResponses(IND_ADDR_RESPONSE, 0, 0, start, responseTimeout, oneAddressOnly, (source, data) -> {
l.add(source);
return Optional.of(source.toByteArray());
});
return l.toArray(IndividualAddress[]::new);
}
@Override
public void writeAddress(final SerialNumber serialNo, final IndividualAddress newAddress)
throws KNXTimeoutException, KNXLinkClosedException {
final byte[] apdu = DataUnitBuilder.apdu(IND_ADDR_SN_WRITE).put(serialNo.array()).put(newAddress.toByteArray())
.putShort(0).putShort(0).build();
broadcast(serialNo, new IndividualAddress(0), false, Priority.SYSTEM, apdu, false);
}
@Override
public IndividualAddress readAddress(final SerialNumber serialNumber)
throws KNXTimeoutException, KNXRemoteException, KNXLinkClosedException, InterruptedException {
final long start = registerActiveService(IND_ADDR_SN_RESPONSE);
tl.broadcast(false, Priority.SYSTEM, createAPDU(IND_ADDR_SN_READ, serialNumber.array()));
final byte[] address = waitForResponses(IND_ADDR_SN_RESPONSE, 10, 10, start, responseTimeout, true,
(source, apdu) -> Arrays.equals(serialNumber.array(), 0, 6, apdu, 2, 8)
? Optional.of(source.toByteArray()) : Optional.empty()).get(0);
return new IndividualAddress(address);
}
@Override
public void writeDomainAddress(final byte[] domain) throws KNXTimeoutException, KNXLinkClosedException {
if (domain.length != 2 && domain.length != 6)
throw new KNXIllegalArgumentException("invalid length of domain address");
broadcast(SerialNumber.Zero, new IndividualAddress(0), true, priority, createAPDU(DOA_WRITE, domain), false);
}
@Override
public void writeDomainAddress(final SerialNumber serialNumber, final byte[] domain)
throws KNXTimeoutException, KNXLinkClosedException {
if (domain.length != 2 && domain.length != 4 && domain.length != 6 && domain.length != 21)
throw new KNXIllegalArgumentException("domain address with invalid length " + domain.length);
final var apdu = DataUnitBuilder.apdu(DomainAddressSerialNumberWrite).put(serialNumber.array()).put(domain).build();
final boolean requireSecure = domain.length == 21;
broadcast(serialNumber, new IndividualAddress(0), true, priority, apdu, requireSecure);
}
private void broadcast(final SerialNumber serialNumber, final IndividualAddress dst, final boolean systemBcast,
final Priority p, final byte[] apdu, final boolean requireSecure)
throws KNXTimeoutException, KNXLinkClosedException {
try {
final var securityCtrl = systemBcast ? SecurityControl.SystemBroadcast
: SecurityControl.of(DataSecurity.AuthConf, true);
final var tsdu = sal.secureBroadcastData(src, serialNumber, dst, apdu, securityCtrl).orElseGet(() -> {
if (requireSecure)
throw new KnxSecureException("broadcast requires data security");
return apdu;
});
tl.broadcast(systemBcast, p, tsdu);
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public List readDomainAddress(final boolean oneDomainOnly)
throws KNXLinkClosedException, KNXInvalidResponseException, KNXTimeoutException,
InterruptedException
{
// we allow 6 bytes ASDU for RF domains
return makeDOAs(readBroadcast(priority, DataUnitBuilder.createLengthOptimizedAPDU(DOA_READ), DOA_RESPONSE,
2, 6, oneDomainOnly));
}
@Override
public void readDomainAddress(final BiConsumer response)
throws KNXLinkClosedException, KNXInvalidResponseException, KNXTimeoutException, InterruptedException
{
final long start = registerActiveService(DOA_RESPONSE);
tl.broadcast(true, priority, DataUnitBuilder.createLengthOptimizedAPDU(DOA_READ));
try {
// we allow 6 bytes ASDU for RF domains
waitForResponses(DOA_RESPONSE, 2, 6, start, responseTimeout, false, (source, apdu1) -> {
response.accept(source, Arrays.copyOfRange(apdu1, 2, apdu1.length));
return Optional.of(apdu1);
});
}
catch (final KNXTimeoutException e) {}
}
@Override
public List readDomainAddress(final byte[] domain, final IndividualAddress start,
final int range) throws KNXInvalidResponseException, KNXLinkClosedException,
KNXTimeoutException, InterruptedException
{
if (domain.length != 2)
throw new KNXIllegalArgumentException("length of domain address not 2 bytes");
if (range < 0 || range > 255)
throw new KNXIllegalArgumentException("range out of range [0..255]");
final var apdu = DataUnitBuilder.apdu(DOA_SELECTIVE_READ).put(domain).put(start.toByteArray()).put(range).build();
return makeDOAs(readBroadcast(priority, apdu, DOA_RESPONSE, 2, 2, false));
}
@Override
public List readNetworkParameter(final IndividualAddress remote, final int objectType, final int pid,
final byte... testInfo)
throws KNXLinkClosedException, KNXTimeoutException, KNXInvalidResponseException, InterruptedException
{
final long start = registerActiveService(NetworkParamResponse);
sendNetworkParameter(NetworkParamRead, remote, objectType, pid, testInfo);
final BiFunction> testResponse = (responder, apdu) -> {
if (apdu.length < 5)
return Optional.empty();
final int receivedIot = (apdu[2] & 0xff) << 8 | (apdu[3] & 0xff);
final int receivedPid = apdu[4] & 0xff;
if (apdu.length == 5) {
final String s = receivedPid == 0xff ? receivedIot == 0xffff ? "object type" : "PID" : "response";
logger.info("network parameter read response from {} for interface object type {} "
+ "PID {}: unsupported {}", responder, objectType, pid, s);
return Optional.empty();
}
return receivedIot == objectType && receivedPid == pid ? Optional.of(apdu) : Optional.empty();
};
try {
final List responses = waitForResponses(NetworkParamResponse, 3, 14, start, responseTimeout,
false, testResponse);
final int prefix = 2 + 3 + testInfo.length;
return responses.stream().map(r -> Arrays.copyOfRange(r, prefix, r.length)).collect(toList());
}
catch (final KNXTimeoutException e) {
return List.of();
}
}
@Override
public List readNetworkParameter(final int objectType, final int pid, final byte... testInfo)
throws KNXLinkClosedException, KNXTimeoutException, KNXInvalidResponseException, InterruptedException {
final long start = registerActiveService(NetworkParamResponse);
sendNetworkParameter(NetworkParamRead, null, objectType, pid, testInfo);
final var responses = new ArrayList();
final BiFunction> testResponse = (responder, apdu) -> {
if (apdu.length < 5)
return Optional.empty();
final int receivedIot = (apdu[2] & 0xff) << 8 | (apdu[3] & 0xff);
final int receivedPid = apdu[4] & 0xff;
if (apdu.length == 5) {
final String s = receivedPid == 0xff ? receivedIot == 0xffff ? "object type" : "PID" : "response";
logger.info("network parameter read response from {} for interface object type {} "
+ "PID {}: unsupported {}", responder, objectType, pid, s);
return Optional.empty();
}
if (receivedIot == objectType && receivedPid == pid) {
final int prefix = 2 + 3 + testInfo.length;
final byte[] testResult = Arrays.copyOfRange(apdu, prefix, apdu.length);
responses.add(new TestResult(responder, testResult));
return Optional.of(apdu);
}
return Optional.empty();
};
try {
waitForResponses(NetworkParamResponse, 3, 14, start, responseTimeout, false, testResponse);
return responses;
}
catch (final KNXTimeoutException e) {
return List.of();
}
}
@Override
public void writeNetworkParameter(final IndividualAddress remote, final int objectType, final int pid,
final byte... value) throws KNXLinkClosedException, KNXTimeoutException
{
sendNetworkParameter(NetworkParamWrite, remote, objectType, pid, value);
}
private void sendNetworkParameter(final int apci, final IndividualAddress remote, final int objectType,
final int pid, final byte[] value) throws KNXTimeoutException, KNXLinkClosedException
{
if (objectType < 0 || objectType > 0xffff || pid < 0 || pid > 0xff)
throw new KNXIllegalArgumentException("IOT or PID argument out of range");
final byte[] asdu = new byte[3 + value.length];
asdu[0] = (byte) (objectType >> 8);
asdu[1] = (byte) objectType;
asdu[2] = (byte) pid;
System.arraycopy(value, 0, asdu, 3, value.length);
final Priority p = Priority.SYSTEM;
final byte[] tsdu = createAPDU(apci, asdu);
if (remote != null)
tl.sendData(remote, p, tsdu);
else
tl.broadcast(true, p, tsdu);
}
@Override
public List readSystemNetworkParameter(final int objectType, final int pid, final int operand,
final byte... additionalTestInfo) throws KNXException, InterruptedException {
if (operand < 0 || operand > 0xfe)
throw new KNXIllegalArgumentException("operand out of range");
final byte[] testInfo = allocate(1 + additionalTestInfo.length).put((byte) operand)
.put(additionalTestInfo).array();
final long start = registerActiveService(SystemNetworkParamResponse);
sendSystemNetworkParameter(SystemNetworkParamRead, objectType, pid, testInfo);
final BiFunction> testParamType = (responder, apdu) -> {
if (apdu.length < 6)
return Optional.empty();
final int receivedIot = (apdu[2] & 0xff) << 8 | (apdu[3] & 0xff);
final int receivedPid = (apdu[4] & 0xff) << 4 | (apdu[5] & 0xf0) >> 4;
if (apdu.length == 6) {
final String s = receivedPid == 0xff ? receivedIot == 0xffff ? "object type" : "PID" : "response";
logger.info("system network parameter read response from {} for interface object type {} "
+ "PID {}: unsupported {}", responder, objectType, pid, s);
return Optional.empty();
}
final int receivedOperand = apdu[6] & 0xff;
return receivedIot == objectType && receivedPid == pid && receivedOperand == operand ? Optional.of(apdu)
: Optional.empty();
};
final Duration waitTime = Duration.ofSeconds(
operand == 1 ? 1 : operand == 2 || operand == 3 ? additionalTestInfo[0] & 0xff
: responseTimeout().toSeconds())
.plusMillis(500); // allow some communication overhead (medium access & device delay times)
try {
final List responders = waitForResponses(SystemNetworkParamResponse, 4, 12, start,
waitTime, false, testParamType);
final int prefix = 2 + 4 + 1 + additionalTestInfo.length;
return responders.stream().map(r -> Arrays.copyOfRange(r, prefix, r.length)).collect(toList());
}
catch (final KNXTimeoutException e) {
return List.of();
}
}
@Override
public void writeSystemNetworkParameter(final int objectType, final int pid, final byte... value)
throws KNXLinkClosedException, KNXTimeoutException {
sendSystemNetworkParameter(SystemNetworkParamWrite, objectType, pid, value);
}
private void sendSystemNetworkParameter(final int apci, final int objectType, final int pid, final byte[] value)
throws KNXTimeoutException, KNXLinkClosedException {
if (objectType < 0 || objectType > 0xffff || pid < 0 || pid > 0xfff)
throw new KNXIllegalArgumentException("IOT or PID argument out of range");
final byte[] asdu = allocate(4 + value.length).putShort((short) objectType).putShort((short) (pid << 4))
.put(value).array();
final byte[] tsdu = createAPDU(apci, asdu);
tl.broadcast(true, Priority.SYSTEM, tsdu);
}
@Override
public byte[] readDeviceDesc(final Destination dst, final int descType)
throws KNXInvalidResponseException, KNXDisconnectException, KNXTimeoutException,
KNXLinkClosedException, InterruptedException
{
if (descType < 0 || descType > 63)
throw new KNXIllegalArgumentException("descriptor type out of range [0..63]");
final byte[] apdu = sendWait(dst, priority, DataUnitBuilder.createLengthOptimizedAPDU(
DEVICE_DESC_READ, (byte) descType), DEVICE_DESC_RESPONSE, 2, 14);
final byte[] dd = new byte[apdu.length - 2];
System.arraycopy(apdu, 2, dd, 0, apdu.length - 2);
return dd;
}
@Override
public void restart(final Destination dst) throws KNXTimeoutException, KNXLinkClosedException, InterruptedException
{
try {
restart(true, dst, null, 0);
}
catch (KNXRemoteException | KNXDisconnectException ignore) {}
}
@Override
public Duration restart(final Destination dst, final EraseCode eraseCode, final int channel) throws KNXTimeoutException,
KNXRemoteException, KNXDisconnectException, KNXLinkClosedException, InterruptedException {
return restart(false, dst, eraseCode, channel);
}
// for erase codes 1,3,4 the channel should be 0
private Duration restart(final boolean basic, final Destination dst, final EraseCode eraseCode,
final int channel) throws KNXTimeoutException, KNXRemoteException, KNXDisconnectException,
KNXLinkClosedException, InterruptedException
{
int time = 0;
if (basic) {
send(dst, priority, DataUnitBuilder.createLengthOptimizedAPDU(RESTART));
}
else {
final byte[] sdu = new byte[] { 0x01, (byte) eraseCode.code(), (byte) channel, };
final byte[] send = DataUnitBuilder.createLengthOptimizedAPDU(RESTART, sdu);
final byte[] apdu = sendWait(dst, priority, send, RESTART, 3, 3);
// check we get a restart response
if ((apdu[1] & 0x32) == 0)
throw new KNXInvalidResponseException("restart response bit not set");
// defined error codes: 0,1,2,3
final String[] codes = new String[] { "Success", "Access Denied",
"Unsupported Erase Code", "Invalid Channel Number", "Unknown Error" };
final int error = Math.min(apdu[2] & 0xff, 4);
if (error > 0)
throw new KNXRemoteException("master reset: " + codes[error]);
time = ((apdu[3] & 0xff) << 8) | (apdu[4] & 0xff);
}
if (dst.isConnectionOriented()) {
// a remote endpoint is allowed to not send a TL disconnect before restart, but
// a TL disconnect timeout shall not be treated as protocol error
final Object lock = new Object();
final TransportListener l = new TLListener()
{
@Override
public void disconnected(final Destination d)
{
if (d.equals(dst))
synchronized (lock) {
lock.notify();
}
}
};
tl.addTransportListener(l);
try {
synchronized (lock) {
while (dst.getState() != Destination.State.Disconnected)
lock.wait();
}
}
finally {
tl.removeTransportListener(l);
}
// always force a disconnect from our side
tl.disconnect(dst);
}
return Duration.ofSeconds(time);
}
@Override
public byte[] readProperty(final Destination dst, final int objIndex, final int propertyId,
final int start, final int elements) throws KNXTimeoutException, KNXRemoteException,
KNXDisconnectException, KNXLinkClosedException, InterruptedException
{
return readProperty(dst, objIndex, propertyId, start, elements, true).get(0);
}
List readProperty(final Destination dst, final int objIndex, final int propertyId, final int start,
final int elements, final boolean oneResponseOnly) throws KNXTimeoutException, KNXRemoteException,
KNXDisconnectException, KNXLinkClosedException, InterruptedException {
if (objIndex < 0 || objIndex > 255 || propertyId < 0 || propertyId > 255
|| start < 0 || start > 0xFFF || elements < 0 || elements > 15)
throw new KNXIllegalArgumentException(String.format("argument value out of range: "
+ "OI 0 < %d < 256, PID 0 < %d < 256, start 0 < %d < 256, elems 0 < %d < 16",
objIndex, propertyId, start, elements));
final int maxAsduLength = maxAsduLength(dst);
int elemsInAsdu = elements;
if (elements > 1) {
final var data = readPropertyDesc(dst, objIndex, propertyId, 0);
final var desc = Description.from(0, data);
final int typeSize = Math.max(8, PropertyTypes.bitSize(desc.pdt()).orElse(8)) / 8;
elemsInAsdu = (maxAsduLength - 4) / typeSize;
}
final List responses = new ArrayList<>();
final List exceptions = new ArrayList<>();
for (int i = 0; i < elements; i += elemsInAsdu) {
final int queryElements = Math.min(elemsInAsdu, elements - i);
final var send = DataUnitBuilder.apdu(PROPERTY_READ).put(objIndex).put(propertyId)
.put((queryElements << 4) | (((start + i) >>> 8) & 0xF)).put(start + i).build();
final long startSend = send(dst, priority, send, PROPERTY_RESPONSE);
final BiFunction> responseFilter = (source, apdu) -> {
try {
if (source.equals(dst.getAddress())) {
responses.add(extractPropertyElements(apdu, objIndex, propertyId, queryElements));
return Optional.of(apdu);
}
}
catch (final KNXInvalidResponseException e) {
logger.debug("skip invalid property read response: {}", e.getMessage());
}
catch (final KNXRemoteException e) {
exceptions.add(e);
return Optional.of(new byte[0]); // return empty token to exit waitForResponses
}
return Optional.empty();
};
waitForResponses(PROPERTY_RESPONSE, 4, maxAsduLength, startSend, responseTimeout, oneResponseOnly,
responseFilter);
}
if (responses.isEmpty()) {
if (exceptions.size() == 1)
throw exceptions.get(0);
final KNXRemoteException e = new KNXRemoteException(
"reading property " + dst.getAddress() + " OI " + objIndex + " PID " + propertyId + " failed");
if (!exceptions.isEmpty())
exceptions.forEach(e::addSuppressed);
throw e;
}
if (oneResponseOnly) {
final var baos = new ByteArrayOutputStream();
responses.forEach(baos::writeBytes);
return List.of(baos.toByteArray());
}
return responses;
}
@Override
public void writeProperty(final Destination dst, final int objIndex, final int propertyId, final int start,
final int elements, final byte[] data) throws KNXTimeoutException, KNXRemoteException, KNXDisconnectException,
KNXLinkClosedException, InterruptedException {
if (objIndex < 0 || objIndex > 255 || propertyId < 0 || propertyId > 255 || start < 0
|| start > 0xFFF || data.length == 0 || elements < 0 || elements > 15)
throw new KNXIllegalArgumentException("argument value out of range");
final byte[] asdu = new byte[4 + data.length];
asdu[0] = (byte) objIndex;
asdu[1] = (byte) propertyId;
asdu[2] = (byte) ((elements << 4) | ((start >>> 8) & 0xF));
asdu[3] = (byte) start;
System.arraycopy(data, 0, asdu, 4, data.length);
final byte[] send = createAPDU(PROPERTY_WRITE, asdu);
final byte[] apdu = sendWait(dst, priority, send, PROPERTY_RESPONSE, 4, maxAsduLength(dst));
// if number of elements is 0, remote app had problems
final int elems = (apdu[4] & 0xFF) >> 4;
if (elems == 0)
throw new KNXRemoteException("property write failed/forbidden");
if (elems != elements)
throw new KNXInvalidResponseException("number of elements differ");
if (data.length != apdu.length - 6)
throw new KNXInvalidResponseException("data lengths differ, bytes: "
+ data.length + " written, " + (apdu.length - 6) + " response");
// explicitly read back written properties
for (int i = 4; i < asdu.length; ++i)
if (apdu[2 + i] != asdu[i])
throw new KNXRemoteException("read back failed (erroneous property data)");
}
private static final int PropertyExtWrite = 0b0111001110;
private static final int PropertyExtWriteResponse = 0b0111001111;
@Override
public void writeProperty(final Destination dst, final int objectType, final int objectInstance,
final int propertyId, final int start, final int elements, final byte[] data) throws KNXException,
InterruptedException {
final long startSend = sendProperty(PropertyExtWrite, PropertyExtWriteResponse, dst, objectType, objectInstance,
propertyId, start, elements, data);
final BiFunction> responseFilter = (responder, apdu) -> {
if (!responder.equals(dst.getAddress()) || apdu.length != 11)
return Optional.empty();
final int receivedIot = (apdu[2] & 0xff) << 8 | (apdu[3] & 0xff);
final int receivedObjInst = (apdu[4] & 0xff) << 4 | (apdu[5] & 0xf0) >> 4;
final int receivedPid = (apdu[5] & 0xf) << 8 | apdu[6] & 0xff;
final int receivedStart = (apdu[8] & 0xff) << 8 | apdu[9] & 0xff;
return receivedIot == objectType && receivedObjInst == objectInstance && receivedPid == propertyId
&& receivedStart == start ? Optional.of(apdu) : Optional.empty();
};
final var response = waitForResponses(PropertyExtWriteResponse, 9, 9, startSend, responseTimeout, true,
responseFilter);
final var returnCode = ReturnCode.of(response.get(0)[8] & 0xff);
if (returnCode != ReturnCode.Success)
throw new KNXRemoteException(format("write property response for %d(%d)|%d: %s",
objectType, objectInstance, propertyId, returnCode.friendly()));
}
private long sendProperty(final int svc, final int svcRes, final Destination dst, final int objectType,
final int objectInstance, final int propertyId, final int start, final int elements, final byte[] data)
throws KNXTimeoutException, KNXDisconnectException, KNXLinkClosedException, InterruptedException {
if (objectType < 0 || objectType > 0xffff || propertyId < 0 || propertyId > 0xfff || start < 0 || start > 0xffff
|| elements < 0 || elements > 255 || data.length == 0)
throw new KNXIllegalArgumentException("argument value out of range");
final int securityObjectType = 17;
final int pidToolKey = 56;
final var deviceToolKeys = sal.security().deviceToolKeys();
byte[] updateToolKey = null;
byte[] oldToolKey = null;
if (objectType == securityObjectType && objectInstance == 1 && propertyId == pidToolKey) {
updateToolKey = data.clone();
oldToolKey = deviceToolKeys.get(dst.getAddress());
}
final var apdu = DataUnitBuilder.apdu(svc).putShort(objectType)
.putShort((objectInstance << 4) | propertyId >> 8).put(propertyId).put(elements).putShort(start)
.put(data).build();
try {
final long s = send(dst, priority, apdu, svcRes, updateToolKey);
updateToolKey = oldToolKey;
return s;
}
catch (KNXException | InterruptedException | RuntimeException e) {
if (updateToolKey != null) {
final var toolKey = oldToolKey;
deviceToolKeys.compute(dst.getAddress(), (__, ___) -> toolKey);
}
throw e;
}
finally {
if (updateToolKey != null)
Arrays.fill(updateToolKey, (byte) 0);
}
}
private static final int PropertyExtDescRead = 0b0111010010;
private static final int PropertyExtDescResponse = 0b0111010011;
private int[] getOrQueryInterfaceObjectList(final Destination dst)
throws KNXTimeoutException, KNXDisconnectException, KNXLinkClosedException, InterruptedException {
final Optional opt = dst.interfaceObjectList();
if (opt.isPresent())
return opt.get();
int[] list = {};
try {
final int elems = unsigned(readProperty(dst, 0, PID.IO_LIST, 0, 1));
list = new int[elems];
// NYI use bigger stride based on supported apdu length
for (int i = 0; i < list.length; i++)
list[i] = unsigned(readProperty(dst, 0, PID.IO_LIST, i + 1, 1));
}
catch (final KNXRemoteException e) {
logger.debug("device {} does not support extended property services ({})", dst.getAddress(), e.toString());
}
dst.setInterfaceObjectList(list);
return list;
}
private static int unsigned(final byte[] data) {
int i = 0;
for (final byte b : data)
i = i << 8 | b & 0xff;
return i;
}
@Override
public Description readPropertyDescription(final Destination dst, final int objectType, final int objInstance,
final int propertyId, final int propertyIndex) throws KNXTimeoutException, KNXRemoteException,
KNXDisconnectException, KNXLinkClosedException, InterruptedException {
if (objectType < 0 || objectType > 0xffff || objInstance < 0 || objInstance > 0xfff || propertyId < 0
|| propertyId > 0xfff || propertyIndex < 0 || propertyIndex > 0xfff)
throw new KNXIllegalArgumentException("argument value out of range");
final int propDescType = 0;
final int pidx = propertyId != 0 ? 0 : propertyIndex;
final byte[] send = DataUnitBuilder.apdu(PropertyExtDescRead).putShort(objectType).put(objInstance >> 4)
.putShort(((objInstance & 0xf) << 12) | propertyId).putShort((propDescType << 12) | pidx).build();
for (int i = 0; i < 2; i++) {
final byte[] apdu = sendWait(dst, priority, send, PropertyExtDescResponse, 15, 15);
final int rcvPropertyId = (((apdu[5] & 0xf) << 8) | (apdu[6] & 0xff));
final int rcvPropDescType = (apdu[7] >> 4) & 0xf;
final int rcvPropertyIdx = (((apdu[7] & 0xf) << 8) | (apdu[8] & 0xff));
final int rcvObjectType = (apdu[2] & 0xff) << 8 | apdu[3] & 0xff;
// make sure the response contains the requested description
final boolean objTypeOk = objectType == rcvObjectType;
final int rcvObjInstance = (apdu[4] & 0xff) << 4 | (apdu[5] & 0xf0) >> 4;
final boolean oiOk = objInstance == rcvObjInstance;
final boolean pidOk = propertyId == 0 || propertyId == rcvPropertyId;
final boolean pidxOk = propertyId != 0 || propertyIndex == rcvPropertyIdx;
final int dptMain = (apdu[9] & 0xff) << 8 | apdu[10] & 0xff;
final int dptSub = (apdu[11] & 0xff) << 8 | apdu[12] & 0xff;
final boolean writeable = (apdu[13] & 0x80) == 0x80;
final int pdt = apdu[13] & 0x2f;
final int maxElems = (apdu[14] & 0xff) << 8 | apdu[15] & 0xff;
final int readLevel = (apdu[16] & 0xf0) >> 4;
final int writeLevel = apdu[16] & 0xf;
if (rcvPropDescType == 0 && dptMain == 0 && dptSub == 0 && !writeable && pdt == 0 && maxElems == 0
&& readLevel == 0 && writeLevel == 0) {
throw new KNXRemoteException("problem with property description request (IOT or PID non-existant?)");
}
if (rcvPropDescType != 0)
throw new KNXRemoteException("property description type " + rcvPropDescType + " not supported");
if (objTypeOk && oiOk && pidOk && pidxOk)
return Description.fromExtended(0, Arrays.copyOfRange(apdu, 2, apdu.length));
logger.warn("wrong description response for {}({})|{} prop idx {} (got {}({})|{} (idx {}))", objectType,
objInstance, propertyId, propertyIndex, rcvObjectType, rcvObjInstance, rcvPropertyId, rcvPropertyIdx);
}
throw new KNXTimeoutException("timeout occurred while waiting for data response");
}
private Description readPropertyExtDescription(final Destination dst, final int objIndex, final int propertyId,
final int propIndex) throws KNXTimeoutException, KNXRemoteException, KNXDisconnectException,
KNXLinkClosedException, InterruptedException {
if (objIndex < 0 || objIndex > 0xfff || propertyId < 0 || propertyId > 0xfff || propIndex < 0 || propIndex > 0xfff)
throw new KNXIllegalArgumentException("argument value out of range");
final var ioList = getOrQueryInterfaceObjectList(dst);
if (ioList.length <= objIndex)
return null;
final int objType = ioList[objIndex];
final int objInstance = 1;
return readPropertyDescription(dst, objType, objInstance, propertyId, propIndex);
}
@Override
public Description readPropertyDescription(final Destination dst, final int objIndex, final int propertyId,
final int propertyIndex) throws KNXTimeoutException, KNXRemoteException, KNXDisconnectException,
KNXLinkClosedException, InterruptedException {
return Description.from(0, readPropertyDesc(dst, objIndex, propertyId, propertyIndex));
}
private static final boolean useExtPropertyServices = false;
@Override
public byte[] readPropertyDesc(final Destination dst, final int objIndex, final int propertyId, final int propIndex)
throws KNXTimeoutException, KNXRemoteException, KNXDisconnectException, KNXLinkClosedException,
InterruptedException {
if (useExtPropertyServices) {
final var desc = readPropertyExtDescription(dst, objIndex, propertyId, propIndex);
if (desc != null)
return desc.toByteArray();
}
if (objIndex < 0 || objIndex > 255 || propertyId < 0 || propertyId > 255 || propIndex < 0 || propIndex > 255)
throw new KNXIllegalArgumentException("argument value out of range");
final byte[] send = DataUnitBuilder.createAPDU(PROPERTY_DESC_READ, (byte) objIndex, (byte) propertyId, (byte) (propertyId == 0 ? propIndex : 0));
for (int i = 0; i < 2; i++) {
final byte[] apdu = sendWait(dst, priority, send, PROPERTY_DESC_RESPONSE, 7, 7);
// make sure the response contains the requested description
final boolean oiOk = objIndex == (apdu[2] & 0xff);
final boolean pidOk = propertyId == 0 || propertyId == (apdu[3] & 0xff);
final boolean pidxOk = propertyId != 0 || propIndex == (apdu[4] & 0xff);
if (oiOk && pidOk && pidxOk) {
// max_nr_elem field is a 4bit exponent + 12bit unsigned
// on problem this field is 0
if (apdu[6] == 0 && apdu[7] == 0)
throw new KNXRemoteException("got no property description (object non-existant?)");
return new byte[] { apdu[2], apdu[3], apdu[4], apdu[5], apdu[6], apdu[7], apdu[8] };
}
logger.warn("wrong description response for OI {} PID {} prop idx {} (got {}|{} (idx {}))", objIndex,
propertyId, propIndex, apdu[2] & 0xff, apdu[3] & 0xff, apdu[4] & 0xff);
}
throw new KNXTimeoutException("timeout occurred while waiting for data response");
}
@Override
public byte[] callFunctionProperty(final Destination dst, final int objectType, final int objInstance,
final int propertyId, final int serviceId, final byte... serviceInfo)
throws KNXException, InterruptedException {
return functionProperty(FunctionPropertyExtCommand, dst, objectType, objInstance, propertyId, serviceId,
serviceInfo);
}
@Override
public byte[] readFunctionPropertyState(final Destination dst, final int objectType, final int objInstance,
final int propertyId, final int serviceId, final byte... serviceInfo)
throws KNXException, InterruptedException {
return functionProperty(FunctionPropertyExtStateRead, dst, objectType, objInstance, propertyId, serviceId,
serviceInfo);
}
private byte[] functionProperty(final int cmd, final Destination dst, final int objectType, final int objInstance,
final int propertyId, final int service, final byte... info)
throws KNXLinkClosedException, KNXDisconnectException, KNXTimeoutException, KNXInvalidResponseException,
KNXRemoteException, InterruptedException {
if (objectType < 0 || objectType > 0xffff || objInstance < 0 || objInstance > 0xfff || propertyId < 0
|| propertyId > 0xfff || service < 0 || service > 0xff)
throw new KNXIllegalArgumentException("argument value out of range");
final var asdu = ByteBuffer.allocate(7 + info.length).putShort((short) objectType)
.put((byte) (objInstance >> 4)).put((byte) (((objInstance & 0xf) << 4) | (propertyId >> 8)))
.put((byte) propertyId).put((byte) 0).put((byte) service).put(info);
final long startSend = send(dst, priority, createAPDU(cmd, asdu.array()), FunctionPropertyExtStateResponse);
final var responses = waitForResponses(FunctionPropertyExtStateResponse, 6, 252, startSend, responseTimeout,
true, (source, apdu) -> {
if (source.equals(dst.getAddress()))
return extractFunctionPropertyExtData(objectType, objInstance, propertyId, apdu);
return Optional.empty();
});
final byte[] response = responses.get(0);
final var returnCode = ReturnCode.of(response[0] & 0xff);
if (returnCode != ReturnCode.Success)
throw new KNXRemoteException(format("function property response for %d(%d)|%d service %d: %s",
objectType, objInstance, propertyId, service, returnCode.description()));
return response;
}
private static Optional extractFunctionPropertyExtData(final int objectType, final int oinstance,
final int propertyId, final byte[] apdu) {
final int receivedIot = (apdu[2] & 0xff) << 8 | (apdu[3] & 0xff);
final int receivedOinst = (apdu[4] & 0xff) << 4 | (apdu[5] & 0xf0) >> 4;
final int receivedPid = (apdu[5] & 0x0f) << 8 | (apdu[6] & 0xff);
return receivedIot == objectType && receivedOinst == oinstance && receivedPid == propertyId
? Optional.of(Arrays.copyOfRange(apdu, 7, apdu.length)) : Optional.empty();
}
@Override
public int readADC(final Destination dst, final int channel, final int repeat)
throws KNXTimeoutException, KNXDisconnectException, KNXRemoteException,
KNXLinkClosedException, InterruptedException
{
if (channel < 0 || channel > 63 || repeat < 0 || repeat > 255)
throw new KNXIllegalArgumentException("ADC arguments out of range");
if (!dst.isConnectionOriented())
throw new KNXIllegalArgumentException("read ADC requires connection-oriented mode: " + dst);
final byte[] apdu = sendWait(dst, priority,
DataUnitBuilder.createLengthOptimizedAPDU(ADC_READ, (byte) channel,
(byte) repeat), ADC_RESPONSE, 3, 3);
if (apdu[2] == 0)
throw new KNXRemoteException("error reading value of A/D converter");
return ((apdu[3] & 0xff) << 8) | apdu[4] & 0xff;
}
@Override
public byte[] readMemory(final Destination dst, final int startAddr, final int bytes)
throws KNXTimeoutException, KNXDisconnectException, KNXRemoteException,
KNXLinkClosedException, InterruptedException
{
final int maxStartAddress = extMemoryServices ? 0xffffff : 0xffff;
final int maxBytes = extMemoryServices ? 248 : 63;
if (startAddr < 0 || startAddr > maxStartAddress || bytes < 1 || bytes > maxBytes)
throw new KNXIllegalArgumentException("argument value out of range");
if (!dst.isConnectionOriented())
throw new KNXIllegalArgumentException("read memory requires connection-oriented mode: " + dst);
// use extended read service for memory access above 65 K
if (startAddr > 0xffff) {
final byte[] send = createAPDU(MemoryExtendedRead,
(byte) bytes, (byte) (startAddr >>> 16), (byte) (startAddr >>> 8), (byte) startAddr);
final byte[] apdu = sendWait(dst, priority, send, MemoryExtendedReadResponse, 4, 252);
final ReturnCode ret = ReturnCode.of(apdu[2] & 0xff);
if (ret != ReturnCode.Success)
throw new KNXRemoteException(
format("read memory from %s 0x%x: %s", dst.getAddress(), startAddr, ret.description()));
return Arrays.copyOfRange(apdu, 6, apdu.length);
}
final byte[] apdu = sendWait(dst, priority,
createLengthOptimizedAPDU(MEMORY_READ,
(byte) bytes, (byte) (startAddr >>> 8), (byte) startAddr),
MEMORY_RESPONSE, 2, 65);
int no = apdu[1] & 0x3F;
if (no == 0)
throw new KNXRemoteException("could not read memory from 0x"
+ Integer.toHexString(startAddr));
final byte[] mem = new byte[no];
while (--no >= 0)
mem[no] = apdu[4 + no];
return mem;
}
@Override
public void writeMemory(final Destination dst, final int startAddr, final byte[] data)
throws KNXDisconnectException, KNXTimeoutException, KNXRemoteException,
KNXLinkClosedException, InterruptedException
{
final int maxStartAddress = extMemoryServices ? 0xffffff : 0xffff;
final int maxBytes = extMemoryServices ? 250 : 63;
if (startAddr < 0 || startAddr > maxStartAddress || data.length == 0 || data.length > maxBytes)
throw new KNXIllegalArgumentException("argument value out of range");
if (!dst.isConnectionOriented())
throw new KNXIllegalArgumentException("write memory requires connection-oriented mode: " + dst);
// use extended write service for memory access above 65 K
if (startAddr > 0xffff) {
final byte[] addrBytes = { (byte) (startAddr >>> 16), (byte) (startAddr >>> 8), (byte) startAddr };
final byte[] asdu = allocate(4 + data.length).put((byte) data.length).put(addrBytes).put(data).array();
final byte[] send = createAPDU(MemoryExtendedWrite, asdu);
final byte[] apdu = sendWait(dst, priority, send, MemoryExtendedWriteResponse, 4, 252);
final ReturnCode ret = ReturnCode.of(apdu[2] & 0xff);
if (ret == ReturnCode.Success)
return;
String desc = ret.description();
if (ret == ReturnCode.SuccessWithCrc) {
final int crc = ((apdu[6] & 0xff) << 8) | (apdu[7] & 0xff);
if (crc16Ccitt(asdu) == crc)
return;
desc = "data verification failed (crc mismatch)";
}
throw new KNXRemoteException(format("write memory to %s 0x%x: %s", dst.getAddress(), startAddr, desc));
}
final byte[] asdu = new byte[data.length + 3];
asdu[0] = (byte) data.length;
asdu[1] = (byte) (startAddr >> 8);
asdu[2] = (byte) startAddr;
System.arraycopy(data, 0, asdu, 3, data.length);
final byte[] send = DataUnitBuilder.createLengthOptimizedAPDU(MEMORY_WRITE, asdu);
if (dst.isVerifyMode()) {
// explicitly read back data
final byte[] apdu = sendWait(dst, priority, send, MEMORY_RESPONSE, 2, 65);
if ((apdu[1] & 0x3f) == 0)
throw new KNXRemoteException("remote app. could not write memory");
if (apdu.length - 4 != data.length)
throw new KNXInvalidResponseException("number of memory bytes differ");
for (int i = 4; i < apdu.length; ++i)
if (apdu[i] != asdu[i - 1])
throw new KNXRemoteException("verify failed (erroneous memory data)");
}
else {
send(dst, priority, send);
}
}
static int crc16Ccitt(final byte[] input) {
final int polynom = 0x1021;
final byte[] padded = Arrays.copyOf(input, input.length + 2);
int result = 0xffff;
for (int i = 0; i < 8 * padded.length; i++) {
result <<= 1;
final int nextBit = (padded[i / 8] >> (7 - (i % 8))) & 0x1;
result |= nextBit;
if ((result & 0x10000) != 0)
result ^= polynom;
}
return result & 0xffff;
}
@Override
public int authorize(final Destination dst, final byte[] key)
throws KNXDisconnectException, KNXTimeoutException, KNXRemoteException,
KNXLinkClosedException, InterruptedException
{
if (key.length != 4)
throw new KNXIllegalArgumentException("length of authorize key not 4 bytes");
if (!dst.isConnectionOriented())
throw new KNXIllegalArgumentException("authorize requires connection-oriented mode: " + dst);
final byte[] asdu = new byte[] { 0, key[0], key[1], key[2], key[3] };
final byte[] apdu = sendWait(dst, priority,
DataUnitBuilder.createAPDU(AUTHORIZE_READ, asdu), AUTHORIZE_RESPONSE, 1, 1);
final int level = apdu[2] & 0xff;
if (level > 15)
throw new KNXInvalidResponseException("authorization level out of range [0..15]");
return level;
}
@Override
public void writeKey(final Destination dst, final int level, final byte[] key)
throws KNXTimeoutException, KNXDisconnectException, KNXRemoteException,
KNXLinkClosedException, InterruptedException
{
// level 255 is free access
if (level < 0 || level > 254 || key.length != 4)
throw new KNXIllegalArgumentException("level out of range or key length not 4 bytes");
if (!dst.isConnectionOriented())
throw new KNXIllegalArgumentException("write key requires connection-oriented mode: " + dst);
final byte[] apdu = sendWait(dst, priority,
DataUnitBuilder.createAPDU(KEY_WRITE, (byte) level, key[0],
key[1], key[2], key[3]), KEY_RESPONSE, 1, 1);
if ((apdu[1] & 0xFF) == 0xFF)
throw new KNXRemoteException("access denied: current access level > write level");
}
@Override
public boolean isOpen()
{
return !detached;
}
@Override
public KNXNetworkLink detach()
{
tl.removeTransportListener(tlListener);
final KNXNetworkLink lnk = detachTransportLayer ? tl.detach() : null;
if (lnk != null) {
logger.debug("detached from {}", lnk);
}
listeners.removeAll();
sal.close();
detached = true;
return lnk;
}
/**
* {@return the transport layer instance used by this management client}
*/
protected TransportLayer transportLayer() { return tl; }
/**
* Sends the supplied {@code apdu} without registering any expected service response,
* optionally encrypting the data unit before sending.
*
* @param d transport layer destination
* @param p message priority
* @param apdu apdu
* @throws KNXTimeoutException
* @throws KNXDisconnectException
* @throws KNXLinkClosedException
* @throws InterruptedException
*/
protected void send(final Destination d, final Priority p, final byte[] apdu)
throws KNXTimeoutException, KNXDisconnectException, KNXLinkClosedException, InterruptedException {
send(d, p, apdu, 0, null);
}
/**
* Sends the supplied {@code apdu} and registers the expected service
* {@code response}, optionally encrypting the data unit before sending.
*
* @param d transport layer destination
* @param p message priority
* @param apdu apdu
* @param responseServiceType expected service response registered before send
* @return timestamp when service response was registered
* @throws KNXTimeoutException
* @throws KNXDisconnectException
* @throws KNXLinkClosedException
* @throws InterruptedException
*/
protected long send(final Destination d, final Priority p, final byte[] apdu, final int responseServiceType)
throws KNXTimeoutException, KNXDisconnectException, KNXLinkClosedException, InterruptedException {
return send(d, p, apdu, responseServiceType, null);
}
/**
* Waits for one or more service responses of type {@code responseServiceType} up to a specified {@code timeout}.
*
* @param responseServiceType response service type
* @param minAsduLen minimum accepted asdu length of a response
* @param maxAsduLen maximum accepted asdu length of a response
* @param start timestamp obtained by {@link #send(Destination, Priority, byte[], int)}
* @param timeout maximum time this method will wait for responses
* @param oneResponseOnly {@code true} if this method should return with the first valid response,
* {@code false} to await the specified {@code timeout}
* @param responseFilter filter a response according to its content, the filter parameters are the response source address and apdu
* @return list of accepted responses
* @throws KNXInvalidResponseException
* @throws KNXTimeoutException
* @throws InterruptedException
*/
protected List waitForResponses(final int responseServiceType, final int minAsduLen, final int maxAsduLen,
final long start, final Duration timeout, final boolean oneResponseOnly,
final BiFunction> responseFilter)
throws KNXInvalidResponseException, KNXTimeoutException, InterruptedException {
long remaining = timeout.toMillis();
final long end = start / 1_000_000L + remaining;
final var responses = new ArrayList();
synchronized (indications) {
while (remaining > 0) {
for (final Iterator i = indications.iterator(); i.hasNext();) {
final var event = i.next();
// purge outdated events
if (start > event.id() + responseTimeout.toNanos()) {
i.remove();
continue;
}
// causality
if (start > event.id())
continue;
final CEMI frame = event.getFrame();
final byte[] apdu = frame.getPayload();
if (responseServiceType != DataUnitBuilder.getAPDUService(apdu))
continue;
final IndividualAddress source = frame instanceof final CEMILData cemild ? cemild.getSource() : new IndividualAddress(0);
if (apdu.length < minAsduLen + 2 || apdu.length > maxAsduLen + 2) {
final String s = "invalid ASDU response length " + (apdu.length - 2) + " bytes, expected "
+ minAsduLen + " to " + maxAsduLen;
logger.warn("received response from " + source + " with " + s);
if (oneResponseOnly)
throw new KNXInvalidResponseException(s);
}
responseFilter.apply(source, apdu).ifPresent(response -> {
responses.add(response);
i.remove();
});
if (!responses.isEmpty() && oneResponseOnly)
return responses;
}
indications.wait(remaining);
remaining = end - System.nanoTime() / 1_000_000L;
}
}
if (responses.isEmpty())
throw new KNXTimeoutException("timeout waiting for data response");
return responses;
}
/**
* Sends the supplied {@code apdu} using the assigned transport layer, optionally encrypting the data unit before
* sending, and waits up to a specified {@code timeout} for a single response of service type {@code responseServiceType}.
*
* @param d transport layer destination
* @param p message priority
* @param apdu apdu
* @param responseServiceType expected service response
* @param minAsduLen minimum accepted asdu length of response
* @param maxAsduLen maximum accepted asdu length of response
* @param timeout maximum time this method will wait for a response
* @return the response apdu
* @throws KNXDisconnectException
* @throws KNXTimeoutException
* @throws KNXInvalidResponseException
* @throws KNXLinkClosedException
* @throws InterruptedException
*/
protected byte[] sendWait(final Destination d, final Priority p, final byte[] apdu, final int responseServiceType,
final int minAsduLen, final int maxAsduLen, final Duration timeout) throws KNXDisconnectException,
KNXTimeoutException, KNXInvalidResponseException, KNXLinkClosedException, InterruptedException {
final long start = send(d, p, apdu, responseServiceType);
return waitForResponse(d.getAddress(), responseServiceType, minAsduLen, maxAsduLen, start, timeout);
}
private int maxApduLength(final Destination dst) throws KNXLinkClosedException, InterruptedException {
final Optional max = dst.maxApduLength();
if (max.isPresent())
return max.get();
final var link = ((TransportLayerImpl) tl).link();
final int maxLinkApdu = link.getKNXMedium().maxApduLength();
int maxDeviceApdu = 15;
// always set a default to avoid multiple queries
dst.maxApduLength(maxDeviceApdu);
if (maxLinkApdu > maxDeviceApdu) {
try {
// note, this read property call already requires a default minimum apdu to be set
final var data = readProperty(dst, 0, PID.MAX_APDULENGTH, 1, 1);
maxDeviceApdu = (data[0] & 0xff) << 8 | data[1] & 0xff;
dst.maxApduLength(Math.min(maxLinkApdu, maxDeviceApdu));
}
catch (KNXTimeoutException | KNXRemoteException | KNXDisconnectException e) {
logger.debug("use max. APDU length of 15 bytes for {}", dst.getAddress());
}
}
return dst.maxApduLength().get();
}
private int maxAsduLength(final Destination dst) throws KNXLinkClosedException, InterruptedException {
return maxApduLength(dst) - 1;
}
private long registerActiveService(final int serviceType) {
final long now = System.nanoTime();
activeServiceResponses.merge(serviceType, now + responseTimeout.toNanos(),
(oldValue, value) -> oldValue < value ? value : oldValue);
return now;
}
private boolean isActiveService(final int serviceType, final long timestamp) {
return activeServiceResponses.computeIfPresent(serviceType,
(__, activeUntil) -> activeUntil < timestamp ? null : activeUntil) != null;
}
private boolean isActiveService(final FrameEvent e) {
return isActiveService(DataUnitBuilder.getAPDUService(e.getFrame().getPayload()), e.id());
}
private long send(final Destination d, final Priority p, final byte[] apdu, final int response,
final byte[] updateToolKey) throws KNXTimeoutException, KNXDisconnectException, KNXLinkClosedException,
InterruptedException {
final long start = response != 0 ? registerActiveService(response) : 0;
final var secCtrl = SecurityControl.of(DataSecurity.AuthConf, toolAccess);
final var sapdu = sal.secureData(src, d.getAddress(), apdu, secCtrl).orElse(apdu);
if (updateToolKey != null) {
sal.security().deviceToolKeys().put(d.getAddress(), updateToolKey);
logger.info("update device toolkey for {}", d.getAddress());
}
if (d.isConnectionOriented())
tl.sendData(d, p, sapdu);
else
tl.sendData(d.getAddress(), p, sapdu);
return start;
}
private byte[] sendWait(final Destination d, final Priority p, final byte[] apdu, final int response,
final int minAsduLen, final int maxAsduLen) throws KNXDisconnectException, KNXTimeoutException,
KNXInvalidResponseException, KNXLinkClosedException, InterruptedException {
return sendWait(d, p, apdu, response, minAsduLen, maxAsduLen, responseTimeout);
}
// min + max ASDU len are *not* including any field that contains ACPI
private byte[] waitForResponse(final IndividualAddress from, final int serviceType, final int minAsduLen,
final int maxAsduLen, final long start, final Duration timeout)
throws KNXInvalidResponseException, KNXTimeoutException, InterruptedException {
return waitForResponses(serviceType, minAsduLen, maxAsduLen, start, timeout, true,
(source, apdu) -> source.equals(from) ? Optional.of(apdu) : Optional.empty()).get(0);
}
private List readBroadcast(final Priority p, final byte[] apdu,
final int response, final int minAsduLen, final int maxAsduLen, final boolean oneOnly)
throws KNXLinkClosedException, KNXInvalidResponseException, KNXTimeoutException,
InterruptedException
{
final long start = registerActiveService(response);
tl.broadcast(true, p, apdu);
return waitForResponses(response, minAsduLen, maxAsduLen, start, responseTimeout, oneOnly,
(source, apdu1) -> Optional.of(apdu1));
}
// cut domain addresses out of APDUs
private static List makeDOAs(final List l)
{
for (int i = 0; i < l.size(); ++i) {
final byte[] pdu = l.get(i);
l.set(i, Arrays.copyOfRange(pdu, 2, pdu.length));
}
return l;
}
// returns property read.res element values
private static byte[] extractPropertyElements(final byte[] apdu, final int objIndex, final int propertyId,
final int elements) throws KNXRemoteException
{
final int oi = apdu[2] & 0xff;
final int pid = apdu[3] & 0xff;
if (oi != objIndex || pid != propertyId)
throw new KNXInvalidResponseException(
String.format("property response mismatch, expected OI %d PID %d (received %d|%d)", objIndex,
propertyId, oi, pid));
// check if number of elements is 0, indicates access problem
final int number = (apdu[4] & 0xFF) >>> 4;
if (number == 0)
throw new KNXRemoteException("property access OI " + oi + " PID " + pid + " failed/forbidden");
if (number != elements)
throw new KNXInvalidResponseException(String.format(
"property access OI %d PID %d expected %d elements (received %d)", oi, pid, elements, number));
final byte[] prop = new byte[apdu.length - 6];
System.arraycopy(apdu, 6, prop, 0, prop.length);
return prop;
}
}