/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.ebus.internal.connection;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import org.openhab.binding.ebus.internal.EBusTelegram;
import org.openhab.binding.ebus.internal.utils.EBusUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Christian Sowada
* @since 1.7.1
*/
public abstract class AbstractEBusWriteConnector extends AbstractEBusConnector {
private static final Logger logger = LoggerFactory.getLogger(AbstractEBusWriteConnector.class);
/** eBUS lockout */
private static int LOCKOUT_COUNTER_MAX = 3;
/** next send try is blocked */
private boolean blockNextSend;
/** last send try caused a collision */
private boolean lastSendCollisionDetected = false;
/** current lockout counter */
private int lockCounter = 0;
/** internal structure to store send attempts */
protected class QueueEntry {
public byte[] buffer;
public int sendAttempts = 0;
public QueueEntry(byte[] buffer) {
this.buffer = buffer;
}
}
/** the send output queue */
private final Queue<QueueEntry> outputQueue = new LinkedBlockingQueue<QueueEntry>(20);
/** default sender id */
private byte senderId = (byte) 0xFF;
/** data to send */
private QueueEntry sendEntry;
/**
* Returns the eBus binding used sender Id
*
* @return
*/
public byte getSenderId() {
return senderId;
}
/**
* Set the eBus binding sender Id
*
* @param senderId The new id, default is 0xFF
*/
public void setSenderId(byte senderId) {
this.senderId = senderId;
}
/*
* (non-Javadoc)
*
* @see org.openhab.binding.ebus.internal.connection.AbstractEBusConnector#connect()
*/
@Override
protected boolean connect() throws IOException {
// reset eBUS counter
lockCounter = LOCKOUT_COUNTER_MAX;
// reset global variables
lastSendCollisionDetected = false;
blockNextSend = false;
outputQueue.clear();
return super.connect();
}
/**
* Add a byte array to send queue.
*
* @param data
* @return
*/
public boolean addToSendQueue(byte[] data) {
if (data == null || data.length == 0) {
logger.trace("Send data is empty, skip");
return false;
}
byte crc = 0;
for (int i = 0; i < data.length - 1; i++) {
byte b = data[i];
crc = EBusUtils.crc8_tab(b, crc);
}
// replace crc with calculated value
data[data.length - 1] = crc;
boolean result = false;
try {
result = outputQueue.add(new QueueEntry(data));
} catch (IllegalStateException e) {
logger.error("Send queue is full! The eBUS service will reset the queue to ensure proper operation.");
outputQueue.clear();
resetSend();
result = outputQueue.add(new QueueEntry(data));
}
return result;
}
private void checkSendStatus() {
if (lockCounter > 0) {
lockCounter--;
}
// blocked for this send slot because a collision
if (blockNextSend) {
logger.trace("Sender was blocked for this SYN ...");
blockNextSend = false;
return;
}
// counter not zero, it's not allowed to send yet
if (lockCounter > 0) {
logger.trace("No access to ebus because the lock counter ...");
return;
}
// currently no data to send
if (outputQueue.isEmpty()) {
return;
}
if (sendEntry != null) {
if (sendEntry.sendAttempts == 10) {
logger.error("Skip telegram {} after 10 attempts ...", EBusUtils.toHexDumpString(sendEntry.buffer));
resetSend();
}
return;
}
// get next entry from stack
sendEntry = outputQueue.peek();
}
/**
* Internal send function. Send and read to detect byte collisions.
*
* @param secondTry
* @throws IOException
*/
protected void send(boolean secondTry) throws IOException {
if (sendEntry == null) {
return;
}
byte[] dataOutputBuffers = sendEntry.buffer;
ByteBuffer sendBuffer = ByteBuffer.allocate(100);
// count as send attempt
sendEntry.sendAttempts++;
int read = 0;
byte readByte = 0;
long readWriteDelay = 0;
// clear input buffer to start by zero
resetInputBuffer();
// send command
for (int i = 0; i < dataOutputBuffers.length; i++) {
byte b = dataOutputBuffers[i];
writeByte(b);
if (i == 0) {
read = readByte(true);
readByte = (byte) (read & 0xFF);
sendBuffer.put(readByte);
if (b != readByte) {
// written and read byte not identical, that's
// a collision
if (readByte == EBusTelegram.SYN) {
logger.debug("eBus collision with SYN detected!");
} else {
logger.debug("eBus collision detected! 0x{}", EBusUtils.toHexDumpString(readByte));
}
// last send try was a collision
if (lastSendCollisionDetected) {
logger.warn("A second collision occured!");
resetSend();
return;
}
// priority class identical
else if ((byte) (readByte & 0x0F) == (byte) (b & 0x0F)) {
logger.trace("Priority class match, restart after next SYN ...");
lastSendCollisionDetected = true;
} else {
logger.trace("Priority class doesn't match, blocked for next SYN ...");
blockNextSend = true;
}
// stop after a collision
return;
}
}
}
// sending master data finish
// reset global variables
lastSendCollisionDetected = false;
blockNextSend = false;
// if this telegram a broadcast?
if (dataOutputBuffers[1] == (byte) 0xFE) {
logger.trace("Broadcast send ..............");
// sende master sync
writeByte(EBusTelegram.SYN);
sendBuffer.put(EBusTelegram.SYN);
} else {
readWriteDelay = System.nanoTime();
// copy input data to result buffer
sendBuffer.put(dataOutputBuffers, 1, dataOutputBuffers.length - 1);
getInputStream().skip(dataOutputBuffers.length - 1);
readWriteDelay = (System.nanoTime() - readWriteDelay) / 1000;
logger.trace("readin delay " + readWriteDelay);
read = readByte(true);
if (read != -1) {
byte ack = (byte) (read & 0xFF);
sendBuffer.put(ack);
if (ack == EBusTelegram.ACK_OK) {
boolean isMasterAddr = EBusUtils.isMasterAddress(dataOutputBuffers[1]);
// if the telegram is a slave telegram we will
// get data from slave
if (!isMasterAddr) {
// len of answer
byte nn2 = (byte) (readByte(true) & 0xFF);
sendBuffer.put(nn2);
byte crc = EBusUtils.crc8_tab(nn2, (byte) 0);
if (nn2 > 16) {
logger.warn("slave data too long, invalid!");
// resend telegram (max. once)
if (!resend(secondTry)) {
return;
}
}
// read slave data, be aware of 0xA9 bytes
while (nn2 > 0) {
byte d = (byte) (readByte(true) & 0xFF);
sendBuffer.put(d);
crc = EBusUtils.crc8_tab(d, crc);
if (d != (byte) 0xA9) {
nn2--;
}
}
// read slave crc
byte crc2 = (byte) (readByte(true) & 0xFF);
sendBuffer.put(crc2);
// check for expanded crc
if (crc2 == (byte) 0xA9) {
// expanded value
crc2 = (byte) (readByte(true) & 0xFF);
sendBuffer.put(crc2);
if (crc2 == (byte) 0x00 && crc == (byte) 0xA9) {
// crc ok, set for next if condition
crc = crc2;
}
if (crc2 == (byte) 0x01 && crc == (byte) 0xAA) {
// crc ok, set for next if condition
crc = crc2;
}
}
// check slave crc
if (crc2 != crc) {
logger.warn("Slave CRC wrong, resend!");
// Resend telegram (max. once)
if (!resend(secondTry)) {
return;
}
}
// sende master sync
writeByte(EBusTelegram.ACK_OK);
sendBuffer.put(EBusTelegram.ACK_OK);
} // isMasterAddr check
// send SYN byte
writeByte(EBusTelegram.SYN);
sendBuffer.put(EBusTelegram.SYN);
} else if (ack == EBusTelegram.ACK_FAIL) {
// clear uncompleted telegram
sendBuffer.clear();
// resend telegram (max. once)
if (!resend(secondTry)) {
return;
}
} else if (ack == EBusTelegram.SYN) {
logger.debug("No answer from slave for telegram: {}", EBusUtils.toHexDumpString(sendBuffer));
// clear uncompleted telegram or it will result
// in uncomplete but valid telegram!
sendBuffer.clear();
// resend instead skip ...
// resetSend();
return;
} else {
// Wow, wrong answer, and now?
logger.debug("Received wrong telegram: {}", EBusUtils.toHexDumpString(sendBuffer));
// clear uncompleted telegram
sendBuffer.clear();
// resend telegram (max. once)
if (!resend(secondTry)) {
return;
}
}
}
}
// after send process the received telegram
if (sendBuffer.position() > 0) {
byte[] buffer = Arrays.copyOf(sendBuffer.array(), sendBuffer.position());
logger.debug("Succcesful send: {}", EBusUtils.toHexDumpString(buffer));
onEBusTelegramReceived(buffer);
}
// reset send module
resetSend();
}
/**
* Resend data if it's the first try or call resetSend()
*
* @param secondTry
* @return
* @throws IOException
*/
private boolean resend(boolean secondTry) throws IOException {
if (!secondTry) {
send(true);
return true;
} else {
logger.warn("Resend failed, remove data from sending queue ...");
resetSend();
return false;
}
}
/**
* Reset the send routine
*/
private void resetSend() {
// reset ebus counter
lockCounter = LOCKOUT_COUNTER_MAX;
// reset global variables
lastSendCollisionDetected = false;
blockNextSend = false;
// remove entry from sending queue
sendEntry = null;
outputQueue.poll();
}
/**
* Called event if a SYN packet has been received
*
* @throws IOException
*/
@Override
protected void onEBusSyncReceived(boolean allowSend) throws IOException {
if (allowSend) {
send(false);
}
// afterwards check for next sending slot
checkSendStatus();
// run read connector
super.onEBusSyncReceived(allowSend);
}
/**
* Writes one byte to the backend
*
* @param b
* @throws IOException
*/
protected abstract void writeByte(int b) throws IOException;
/**
* Resets the input buffer of input stream
*
* @throws IOException
*/
protected abstract void resetInputBuffer() throws IOException;
/**
* Return the undelaying input stream
*
* @return
*/
protected abstract InputStream getInputStream();
}