tuwien.auto.calimero.serial.TpuartConnection Maven / Gradle / Ivy
Show all versions of calimero-core Show documentation
/*
Calimero 2 - A library for KNX network access
Copyright (c) 2014, 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.serial;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.Connection;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXAckTimeoutException;
import tuwien.auto.calimero.KNXAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.KNXListener;
import tuwien.auto.calimero.cemi.CEMI;
import tuwien.auto.calimero.cemi.CEMIBusMon;
import tuwien.auto.calimero.cemi.CEMIFactory;
import tuwien.auto.calimero.cemi.CEMILData;
import tuwien.auto.calimero.internal.EventListeners;
import tuwien.auto.calimero.internal.Executor;
import tuwien.auto.calimero.log.LogService;
import tuwien.auto.calimero.log.LogService.LogLevel;
import tuwien.auto.calimero.serial.spi.SerialCom;
/**
* Provides a connection with a TP-UART-IC controller for transparent communication with a KNX TP1 network. The
* connection supports cEMI L-Data communication and busmonitor mode.
* The host establishes a connection over a serial port, using any identifier recognized and supported by the serial
* adapter and the operating system.
*
* Interruption policy: any blocking sends are cancelled.
*
* @author B. Malinowsky
*/
public class TpuartConnection implements Connection
{
// UART services
private static final int Reset_req = 0x01;
private static final int Reset_ind = 0x03;
// we can use this service to distinguish TP-UART 1/2
// TP-UART 1 doesn't support this service
// private static final int ProductId_req = 0x20;
// the ProductId_res sucks, as it is specified without msg code
// | Bit 7 - 5 | Bit 4 - 0 |
// | Prod. Id | Rev. no |
// TP-UART 2 Release a
// private static final int V2ReleaseA = 0b01000001;
private static final int State_req = 0x02;
private static final int State_ind = 0x07;
private static final int ActivateBusmon = 0x05;
private static final int LData_con = 0x0b; // MSB is pos/neg confirm
private static final int AckInfo = 0x10;
private static final int LDataStart = 0x80; // plus L_Data byte index
private static final int LDataEnd = 0x40; // plus L_Data.length
// TP1 frame type constants
private static final int AlwaysSet = 0x10;
private static final int StdFrameFormat = 0x80 | AlwaysSet;
private static final int ExtFrameFormat = AlwaysSet;
private static final int RepeatFlag = 0x20;
// Short acks in bus monitor mode
private static final int Ack = 0xcc;
private static final int Nak = 0x0c;
private static final int Busy = 0xc0;
// TP-UART send
// time interval to check on TP UART state
private static final long UartStateReadInterval = 5_000_000; // [us]
private static final int UartBaudRate = 19_200;
private static final int Tp1BaudRate = 9_600;
private static final int OneBitTime = (int) Math.ceil(1d / Tp1BaudRate * 1_000_000);
private static final int BitTimes_50 = 50 * OneBitTime; // [us]
private static final int MaxSendAttempts = 4;
private final String portId;
private final SerialCom com;
private final OutputStream os;
private final InputStream is;
private final Receiver receiver = new Receiver();
private final ReentrantLock lock = new ReentrantLock();
private final Condition con = lock.newCondition();
private final Condition enterIdle = lock.newCondition();
private volatile boolean idle;
// NYI compare to received frame for .con, or just remove
private volatile byte[] req;
private volatile boolean busmon;
private volatile int busmonSequence;
private final EventListeners listeners = new EventListeners<>();
private final Set addresses = Collections.synchronizedSet(new HashSet<>());
private final Map sending = new ConcurrentHashMap<>();
private final Logger logger;
/**
* Creates a new TP-UART connection using communication port {@code portId}, expecting a collection of KNX
* addresses for which the host shall acknowledge TP1 frame reception.
*
* @param portId the identifier of the communication port
* @param acknowledge a (possibly empty) collection of KNX addresses this endpoint shall issue a positive
* acknowledgement for, on receiving a valid TP1 frame with its destination address being an element in
* {@code acknowledge}. By default, this endpoint also won't acknowledge the default individual
* address 0.2.ff on the TP1 network.
* @throws KNXException on error opening the communication port, or initializing the TP-UART controller
*/
public TpuartConnection(final String portId, final Collection extends KNXAddress> acknowledge) throws KNXException
{
this.portId = portId;
logger = LogService.getAsyncLogger("calimero.serial.tpuart:" + portId);
com = SerialConnectionFactory.open(portId, UartBaudRate, Duration.ZERO, Duration.ofMillis(5));
os = com.outputStream();
is = com.inputStream();
addresses.add(GroupAddress.Broadcast);
addresses.addAll(acknowledge);
Executor.execute(receiver, "Calimero TP-UART receiver");
try {
reset();
}
catch (final IOException e) {
closeResources();
throw new KNXPortClosedException("on resetting TP-UART controller", portId, e);
}
if (!waitForInitialUartState()) {
closeResources();
throw new KNXPortClosedException("timeout waiting for initial TP-UART state", portId);
}
}
private boolean waitForInitialUartState() {
logger.trace("wait for initial TP-UART state");
long now = System.nanoTime();
final long end = now + 1_000_000_000L;
try {
while (now < end) {
if (receiver.lastUartState != 0)
return true;
Thread.sleep(10);
now = System.nanoTime();
}
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
/**
* Adds the specified event listener {@code l} to receive events from this connection. If {@code l} was
* already added as listener, no action is performed.
*
* @param l the listener to add
*/
@Override
public final void addConnectionListener(final KNXListener l)
{
listeners.add(l);
}
/**
* Removes the specified event listener {@code l}, so it does no longer receive events from this connection. If
* {@code l} was not added in the first place, no action is performed.
*
* @param l the listener to remove
*/
@Override
public final void removeConnectionListener(final KNXListener l)
{
listeners.remove(l);
}
@Override
public String name()
{
return portId;
}
/**
* Activate busmonitor mode, the TP-UART controller is set completely passive.
*
* @throws IOException on I/O error
*/
public void activateBusmonitor() throws IOException
{
logger.debug("activate TP-UART busmonitor");
os.write(ActivateBusmon);
busmonSequence = 0;
busmon = true;
}
/**
* Adds an address to the list of addresses acknowledged on the bus.
*
* @param ack the address to acknowledge
*/
public final void addAddress(final KNXAddress ack)
{
addresses.add(ack);
}
/**
* Removes an address from the list of addresses acknowledged on the bus.
*
* @param ack the address to no further acknowledge
*/
public final void removeAddress(final KNXAddress ack)
{
addresses.remove(ack);
}
@Override
public void send(final byte[] frame, final BlockingMode blockingMode)
throws KNXPortClosedException, KNXAckTimeoutException, InterruptedException {
send(frame, blockingMode != BlockingMode.NonBlocking);
}
/**
* Sends a cEMI L-Data frame, either waiting for confirmation or non-blocking. Sending is not-permitted in
* busmonitor mode. A cEMI frame for TP1 does not require any additional information, any additional information is
* ignored.
*
* @param frame cEMI L-Data msg as byte array
* @param waitForCon wait for L_Data.con (blocking) or not (non-blocking)
* @throws KNXPortClosedException on closed communication port
* @throws KNXAckTimeoutException on send/receive timeout (or if no ACK from the bus was received)
* @throws InterruptedException on thread interrupt, a send waiting for L-Data confirmation will be cancelled
*/
// TODO sync concurrent sends
public void send(final byte[] frame, final boolean waitForCon)
throws KNXPortClosedException, KNXAckTimeoutException, InterruptedException
{
try {
final byte[] tp1Frame = cEmiToTP1(frame);
final byte[] data = toUartServices(tp1Frame);
if (logger.isTraceEnabled())
logger.trace("create UART services {}", DataUnitBuilder.toHex(data, " "));
req = frame.clone();
// force cool down period if we got a crispy chip
final long coolDownMillis = (receiver.coolDownUntil - System.nanoTime()) / 1_000_000;
if (coolDownMillis > 0)
Thread.sleep(coolDownMillis);
final long start = System.nanoTime();
final boolean group = (frame[3] & 0x80) == 0x80;
if (group)
sending.put(new GroupAddress(new byte[] { frame[6], frame[7] }), start);
final boolean logReadyForSending = !idle;
lock.lock();
try {
if (!idle)
enterIdle.await();
}
finally {
lock.unlock();
}
if (logReadyForSending)
logger.trace("UART ready for sending after {} us", (System.nanoTime() - start) / 1000);
logger.debug("write UART services, {}", (waitForCon ? "waiting for .con" : "non-blocking"));
lock.lock();
try {
os.write(data);
if (!waitForCon)
return;
if (waitForCon(tp1Frame.length))
return;
}
finally {
lock.unlock();
}
throw new KNXAckTimeoutException("no ACK for L-Data.con");
}
catch (final InterruptedIOException e) {
throw new InterruptedException(e.getMessage());
}
catch (final IOException e) {
close();
throw new KNXPortClosedException("I/O error", portId, e);
}
finally {
req = null;
}
}
@Override
public void close()
{
close(CloseEvent.USER_REQUEST, "user request");
}
private void close(final int origin, final String reason)
{
// best-effort, as we might already have hit an I/O error
try {
reset();
}
catch (final InterruptedIOException e) {
Thread.currentThread().interrupt();
}
catch (final IOException ignore) {}
closeResources();
fireConnectionClosed(origin, reason);
}
private void closeResources() {
receiver.quit();
com.close();
}
private void fireConnectionClosed(final int origin, final String reason)
{
final CloseEvent ce = new CloseEvent(this, origin, reason);
listeners.fire(l -> l.connectionClosed(ce));
}
private void reset() throws IOException
{
logger.debug("reset TP-UART controller");
busmon = false;
busmonSequence = 0;
os.write(Reset_req);
}
// returns a TP1 std or ext frame
private static byte[] cEmiToTP1(final byte[] frame)
{
// set frame type to std/ext
final int stdMaxApdu = 15;
// skip 1 byte mc + 1 byte add.info length + any add.info
final int skipToCtrl1 = 2 + frame[1] & 0xff;
final int cemiPrefix = skipToCtrl1 + 8;
final boolean extended = (frame[skipToCtrl1] & 0x80) == 0;
final boolean std = !extended && frame.length <= cemiPrefix + stdMaxApdu;
final byte[] tp1;
if (std) {
// total length = frame length - skipToCtrl1 - ctrl2 + 1 byte checksum
tp1 = new byte[frame.length - skipToCtrl1];
int i = 0;
// set upper 6 bits of ctrl1 field, ensure not repeated std frame
tp1[i++] = (byte) ((frame[skipToCtrl1] & 0xfc) | StdFrameFormat | RepeatFlag);
// src
tp1[i++] = frame[skipToCtrl1 + 2];
tp1[i++] = frame[skipToCtrl1 + 3];
// dst
tp1[i++] = frame[skipToCtrl1 + 4];
tp1[i++] = frame[skipToCtrl1 + 5];
// address type, npci, length
final int len = frame[skipToCtrl1 + 6];
tp1[i++] = (byte) ((frame[skipToCtrl1 + 1] & 0xf0) | len);
// tpci
tp1[i++] = frame[skipToCtrl1 + 7];
// apdu
for (int k = 0; k < len; k++)
tp1[i++] = frame[cemiPrefix + k];
}
else {
// total length = frame length - skipToCtrl1 + 1 byte checksum
final int length = frame.length - skipToCtrl1 + 1;
if (length > 64)
throw new KNXIllegalArgumentException("L-Data frame length " + length + " > max. 64 bytes for TP-UART");
tp1 = new byte[length];
System.arraycopy(frame, skipToCtrl1, tp1, 0, frame.length - skipToCtrl1);
// ensure not repeated ext frame
tp1[0] &= ~StdFrameFormat;
tp1[0] |= ExtFrameFormat | RepeatFlag;
}
// last byte is checksum
tp1[tp1.length - 1] = (byte) checksum(tp1);
return tp1;
}
private static byte[] toUartServices(final byte[] tp1)
{
final ByteArrayOutputStream data = new ByteArrayOutputStream(tp1.length * 2);
for (int i = 0; i < tp1.length - 1; i++) {
data.write(LDataStart | i);
data.write(tp1[i]);
}
// write end data with frame checksum
data.write(LDataEnd | tp1.length - 1);
data.write(tp1[tp1.length - 1]);
return data.toByteArray();
}
private boolean waitForCon(final int frameLen) throws InterruptedException
{
// time from start-bit to start-bit of inner frame consecutive characters, 13 bit times [us]
final int innerFrameChar = 13 * OneBitTime;
final int bitTimes_15 = 15 * OneBitTime; // [us]
final int maxExchangeTimeout = MaxSendAttempts * (BitTimes_50 + frameLen * innerFrameChar + 2 * bitTimes_15) / 1000;
final long start = System.nanoTime();
final boolean rcvdCon = con.await(maxExchangeTimeout, TimeUnit.MILLISECONDS);
final long wait = (System.nanoTime() - start) / 1_000_000;
if (rcvdCon)
logger.trace("ACK received after {} ms", wait);
else
logger.debug("no ACK received after {} ms", wait);
return rcvdCon;
}
private static int checksum(final byte[] frame)
{
int cs = 0;
for (final byte b : frame)
cs ^= b;
return ~cs;
}
private boolean isValidChecksum(final byte[] frame)
{
final byte[] copy = Arrays.copyOf(frame, frame.length - 1);
final int cs = checksum(copy);
final int expected = frame[frame.length - 1];
final boolean valid = expected == cs;
if (!valid)
logger.warn("invalid L-Data checksum 0x{}, expected 0x{}", Integer.toHexString(cs & 0xff),
Integer.toHexString(expected & 0xff));
return valid;
}
// Stores the currently used max. inter-byte delay, to also be available for subsequent tpuart connections.
// Defaults to 50 bit times [us]
private static final AtomicInteger maxInterByteDelay = new AtomicInteger(BitTimes_50);
static {
final var key = "calimero.serial.tpuart.maxInterByteDelay";
try {
final var delay = System.getProperty(key);
if (delay != null) {
final int value = Integer.parseUnsignedInt(delay);
maxInterByteDelay.set(value);
LoggerFactory.getLogger("calimero.serial.tpuart").info("using {} of {} us", key, value);
}
}
catch (final RuntimeException e) {
LoggerFactory.getLogger("calimero.serial.tpuart").warn("on checking property {}", key, e);
}
}
private final class Receiver implements Runnable
{
private volatile boolean quit;
private volatile Thread thread;
private final ByteArrayOutputStream in = new ByteArrayOutputStream();
private long lastRead;
private boolean extFrame;
private boolean frameAcked;
private byte[] lastReceived = new byte[0];
volatile long lastUartState; // [us]
private boolean uartStatePending;
private int maxDelay = maxInterByteDelay.get();
private int consecutiveFrameDrops = -1;
private long coolDownUntil;
@Override
public void run()
{
thread = Thread.currentThread();
// at first drain rx queue of any old frames
// most likely, flushes out the reset.ind corresponding to our init reset, but that's ok
int drained = 0;
try {
while (is.read() != -1)
drained++;
}
catch (final IOException ignore) {}
logger.trace("drained rx queue ({} bytes)", drained);
long enterIdleTimestamp = 0; // [ns]
while (!quit) {
try {
final long start = System.nanoTime();
final int c = is.read();
if (c == -1) {
checkUartState();
// we transition to idle state after some time of inactivity, and notify a waiting sender
final long inactivity = 10_000; // [us]
if (enterIdleTimestamp == 0)
enterIdleTimestamp = start;
else if (coolDownUntil > start) {
// we received a temperature warning and are in cool down mode (no sending allowed)
// throttle busy wait on input stream
Thread.sleep(1);
}
else if ((start - enterIdleTimestamp) / 1000 > inactivity) {
lock.lock();
try {
idle = true;
enterIdle.signal();
}
finally {
lock.unlock();
}
}
continue;
}
final long idlePeriod = (start - enterIdleTimestamp) / 1000;
if (enterIdleTimestamp != 0 && idlePeriod > 100_000)
logger.trace("receiver woke from extended idle period of {} us", idlePeriod);
idle = false;
enterIdleTimestamp = 0;
if (parseFrame(c) || isLDataCon(c) || isUartStateInd(c))
; // nothing to do
else if (c == Reset_ind) {
uartStatePending = false;
logger.debug("TP-UART reset.ind");
}
final long loop = System.nanoTime() - start;
logger.trace("loop time = {} us", loop / 1000);
}
catch (final RuntimeException e) {
logger.warn("continue after internal error in receiver loop", e);
}
catch (final InterruptedException e) {}
catch (final IOException e) {
if (!quit)
close(CloseEvent.INTERNAL, "receiver communication failure, " + e);
break;
}
}
}
void quit()
{
quit = true;
final var t = thread;
if (t == null)
return;
t.interrupt();
if (Thread.currentThread() == t)
return;
try {
t.join(50);
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private int maxInterByteDelay()
{
// cond: consecutively losing 4 frames (1 msg w/ 1 .ind + 2 .ind repetitions, and 1st .ind of next msg)
if (consecutiveFrameDrops >= 3) {
maxDelay = maxInterByteDelay.accumulateAndGet(Math.min(maxDelay + 500, 20_000), Math::max);
logger.warn("{} partial frames discarded, increase max. inter-byte delay to {} us",
consecutiveFrameDrops + 1, maxDelay);
consecutiveFrameDrops = -1;
}
return maxDelay;
}
private boolean parseFrame(final int c) throws IOException
{
final long now = System.nanoTime() / 1000;
int size = in.size();
if (size > 0) {
// empty current buffer if we didn't receive data for some time
final int minLength = extFrame ? 7 : 6;
final long diff = now - lastRead;
if (size < minLength && diff > maxInterByteDelay()) {
resetReceiveBuffer(c, diff);
size = 0;
}
else if (size >= minLength && diff > 4L * maxInterByteDelay()) {
resetReceiveBuffer(c, diff);
size = 0;
}
}
if (size > 0) {
in.write(c);
lastRead = now;
final int minLength = extFrame ? 7 : 6;
if (size + 1 >= minLength) {
final byte[] frame = in.toByteArray();
ack(frame);
// check if we got the expected frame size
final int total;
if (extFrame)
total = 8 + (frame[6] & 0x3f) + 1;
else
total = 7 + (frame[5] & 0x0f) + 1;
if (frame.length >= total) {
try {
final byte[] data = in.toByteArray();
logger.debug("received TP1 L-Data (length {}): {}", frame.length,
DataUnitBuilder.toHex(data, " "));
consecutiveFrameDrops = -1;
if (busmon) {
fireFrameReceived(createBusmonInd(data));
}
else {
// check repetition of a directly preceding correctly received frame
final boolean repeated = (data[0] & RepeatFlag) == 0;
if (repeated && Arrays.equals(lastReceived, 0, lastReceived.length - 2, frame, 0,
data.length - 2)) {
logger.debug("ignore repetition of directly preceding correctly received frame");
}
else {
lastReceived = data.clone();
lastReceived[0] &= ~RepeatFlag; // set repeat flag
fireFrameReceived(createLDataInd(data));
}
}
}
catch (final Exception e) {
logger.error("error creating {} from TP1 data (length {}): {}",
busmon ? "Busmon.ind" : "L-Data", frame.length, DataUnitBuilder.toHex(frame, " "),
e);
}
finally {
in.reset();
}
}
}
}
else if (isLDataStart(c)) {
lastRead = now;
in.reset();
in.write(c);
frameAcked = false;
final byte[] minBytes = new byte[extFrame ? 6 : 5];
final int read = is.read(minBytes);
final long initialMinBytes = (System.nanoTime() / 1000 - lastRead);
if (read > 0) {
in.write(minBytes, 0, read - 1);
lastRead = System.nanoTime() / 1000;
parseFrame(minBytes[read - 1]);
}
logger.trace("finished reading {} bytes after {} us", read, initialMinBytes);
}
// busmon mode only: short acks
else if (c == Ack || c == Nak || c == Busy)
fireFrameReceived(createBusmonInd(new byte[] { (byte) c }));
else
return false;
return true;
}
private void resetReceiveBuffer(final int c, final long diff) {
final byte[] buf = in.toByteArray();
in.reset();
logger.debug("reset receive buffer after {} us, char 0x{}, discard partial frame (length {}) {}", diff,
Integer.toHexString(c), buf.length,
DataUnitBuilder.toHex(buf, " "));
consecutiveFrameDrops++;
}
private void checkUartState() throws IOException {
// TP-UART-IC doesn't respond to State.req if busmonitor mode is active
if (busmon)
return;
final long now = System.nanoTime() / 1000;
if (lastUartState + UartStateReadInterval < now) {
if (lastUartState != 0 && now > lastUartState + 2 * UartStateReadInterval + 100_000)
close(CloseEvent.INTERNAL, "UART state communication not possible (TP1 medium not connected?)");
else
readUartState();
}
}
private void readUartState() throws IOException
{
if (uartStatePending)
return;
uartStatePending = true;
os.write(State_req);
}
private boolean isUartStateInd(final int c)
{
if (!uartStatePending)
return false;
final boolean ind = (c & State_ind) == State_ind;
if (!ind)
return false;
uartStatePending = false;
lastUartState = System.nanoTime() / 1000;
final boolean slaveCollision = (c & 0x80) == 0x80;
final boolean rxError = (c & 0x40) == 0x40; // checksum, parity, bit error
final boolean txError = (c & 0x20) == 0x20; // send 0, receive 1
final boolean protError = (c & 0x10) == 0x10; // illegal ctrl byte
final boolean tempWarning = (c & 0x08) == 0x08; // too hot
final boolean info = slaveCollision || rxError || txError || protError || tempWarning;
LogService.log(logger, info ? LogLevel.INFO : LogLevel.TRACE, "TP-UART status: Temp. {}{}{}{}{}",
tempWarning ? "warning" : "OK", slaveCollision ? ", slave collision" : "",
rxError ? ", receive error" : "", txError ? ", transmit error" : "",
protError ? ", protocol error" : "");
if (tempWarning) {
coolDownUntil = System.nanoTime() + 1_000_000_000;
logger.warn("TP-UART high temperature warning! Sending is paused for 1 second ...");
}
return true;
}
private boolean isLDataCon(final int c)
{
final boolean con = (c & 0x7f) == LData_con;
if (con) {
final boolean pos = (c & 0x80) == 0x80;
final String status = pos ? "positive" : "negative";
logger.debug("{} L_Data.con", status);
onConfirmation(pos);
}
return con;
}
private boolean isLDataStart(final int c)
{
if ((c & 0x03) != 0)
return false;
final boolean start = (c & 0xd0) == StdFrameFormat || (c & 0xd0) == ExtFrameFormat;
if (start) {
// final boolean repeated = (c & RepeatFlag) == 0;
// logger.trace("start of frame 0x{}, repeated = {}", Integer.toHexString(c), repeated);
extFrame = (c & 0xd0) == ExtFrameFormat;
}
return start;
}
// pre: we have a new .ind frame, length > 5
// The ack service has to be sent at the latest 1.7 ms after receiving the
// address-type bit of the L-Data.ind
private void ack(final byte[] frame) throws IOException
{
if (busmon || frameAcked)
return;
final int addrOffset = extFrame ? 4 : 3;
final byte[] addr = new byte[] { frame[addrOffset], frame[addrOffset + 1] };
final int addrTypeOffset = extFrame ? 1 : 5;
final boolean group = (frame[addrTypeOffset] & 0x80) == 0x80;
final KNXAddress dst = group ? new GroupAddress(addr) : new IndividualAddress(addr);
// We can answer as follows:
// ACK: if we got addressed
// NAK: we don't care about that, because the TP-UART checks that for us
// Busy: we're never busy
int ack = AckInfo;
final long timestamp = sending.getOrDefault(dst, 0L);
final boolean groupResponse = (System.nanoTime() - timestamp) < 3_000_000_000L;
if (timestamp > 0 && !groupResponse)
sending.remove(dst);
final boolean oneOfUs = addresses.contains(dst) || groupResponse;
if (oneOfUs) {
ack |= 0x01;
os.write(new byte[] { (byte) ack });
logger.trace("write ACK (0x{}) for {}", Integer.toHexString(ack), dst);
}
frameAcked = true;
}
// TODO We assemble a .con using our saved .req, and let the TP-UART L-Data frame bubble up
// as .ind, which is useless. Use the received frame as L-Data.con, or throw it away.
private void onConfirmation(final boolean pos)
{
// assemble a .con frame
final byte[] frame = req;
if (frame == null)
return;
frame[0] = CEMILData.MC_LDATA_CON;
if (pos) {
// set confirm bit to no error
frame[2] &= 0xfe;
}
else {
// set confirm bit to error
frame[2] |= 0x01;
}
lock.lock();
try {
con.signalAll();
}
finally {
lock.unlock();
}
fireFrameReceived(frame);
}
// create a cEMI L-Data from a TP1 frame
private byte[] createLDataInd(final byte[] tp1)
{
return createLData(CEMILData.MC_LDATA_IND, tp1);
}
// create a cEMI L-Data from a TP1 frame
private byte[] createLData(final int mc, final byte[] tp1)
{
if (!isValidChecksum(tp1))
return null;
final boolean std = (tp1[0] & StdFrameFormat) == StdFrameFormat;
final byte[] ind = new byte[tp1.length + (std ? 2 : 1)];
ind[0] = (byte) mc;
// with TP1, there is no additional information, i.e., 2nd cEMI byte is always 0
ind[1] = 0;
if (std) {
ind[2] = tp1[0];
ind[3] = (byte) (tp1[5] & 0xf0);
// src
ind[4] = tp1[1];
ind[5] = tp1[2];
// dst
ind[6] = tp1[3];
ind[7] = tp1[4];
// len
final int len = tp1[5] & 0x0f;
ind[8] = (byte) len;
// tpci
ind[9] = tp1[6];
// apdu
System.arraycopy(tp1, 7, ind, 10, len);
}
else {
System.arraycopy(tp1, 0, ind, 2, tp1.length - 1);
}
return ind;
}
private byte[] createBusmonInd(final byte[] tp1)
{
final int seq = busmonSequence;
busmonSequence = (seq + 1) % 8;
// provide 32 bit timestamp with 1 us precision
final long timestamp = (System.nanoTime() / 1000) & 0xFFFFFFFFL;
// NYI we could at least set frame error in status
return CEMIBusMon.newWithSequenceNumber(seq, timestamp, true, tp1).toByteArray();
}
private void fireFrameReceived(final byte[] frame)
{
if (frame == null)
return;
logger.trace("cEMI (length {}): {}", frame.length, DataUnitBuilder.toHex(frame, " "));
try {
final CEMI msg = CEMIFactory.create(frame, 0, frame.length);
final FrameEvent fe = new FrameEvent(this, msg);
listeners.fire(l -> l.frameReceived(fe));
}
catch (final KNXFormatException | RuntimeException e) {
logger.error("invalid frame for cEMI: {}", DataUnitBuilder.toHex(frame, " "), e);
}
}
}
}