/*
* Copyright 2014 Bevbot LLC <info@bevbot.com>
*
* This file is part of the Kegtab package from the Kegbot project. For
* more information on Kegtab or Kegbot, see <http://kegbot.org/>.
*
* Kegtab 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, version 2.
*
* Kegtab 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 Kegtab. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kegbot.kegboard;
import com.google.common.collect.Maps;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Shorts;
import java.util.Arrays;
import java.util.Map;
/**
* Base message type for messages to/from a Kegboard device.
*
* @author mike wakerly (opensource@hoho.com)
* @see <a href="http://kegbot.org/docs/kegboard-guide/">Kegboard Guide</a>
*/
public abstract class KegboardMessage {
private static final int KBSP_HEADER_LENGTH = 12;
private static final int KBSP_TRAILER_LENGTH = 4;
private static final int KBSP_MIN_LENGTH = KBSP_HEADER_LENGTH
+ KBSP_TRAILER_LENGTH;
private static final byte[] KBSP_HEADER_BYTES = "KBSP v1:".getBytes();
private static final byte[] KBSP_TRAILER_BYTES = {'\r', '\n'};
private static final int KBSP_MAX_LENGTH = 256;
private static final int KBSP_PAYLOAD_MAX_LENGTH = KBSP_MAX_LENGTH - KBSP_MIN_LENGTH;
protected final Map<Integer, byte[]> mTags = Maps.newLinkedHashMap();
protected KegboardMessage() {
assert (false);
}
protected KegboardMessage(byte[] wholeMessage) throws KegboardMessageException {
if (wholeMessage.length < KBSP_MIN_LENGTH) {
throw new KegboardMessageException("Raw message size too small: min=" + KBSP_MIN_LENGTH
+ ", actual=" + wholeMessage.length);
} else if (wholeMessage.length > KBSP_MAX_LENGTH) {
throw new KegboardMessageException("Raw message size too large: max=" + KBSP_MAX_LENGTH
+ ", actual=" + wholeMessage.length);
}
final int payloadLength = Shorts.fromBytes(wholeMessage[11], wholeMessage[10]) & 0x0ffff;
final int payloadEnd = KBSP_HEADER_LENGTH + payloadLength;
if (payloadLength > KBSP_PAYLOAD_MAX_LENGTH) {
throw new KegboardMessageException("Illegal payload size: max=" + KBSP_PAYLOAD_MAX_LENGTH
+ ", actual=" + payloadLength);
}
final int totalMessageSize = KBSP_HEADER_LENGTH + payloadLength + KBSP_TRAILER_LENGTH;
if (wholeMessage.length != totalMessageSize) {
throw new KegboardMessageException("Input buffer size does not match computed size: "
+ "payloadLength=" + payloadLength + ", needed=" + totalMessageSize
+ ", actual=" + wholeMessage.length);
}
final byte[] payload;
if (payloadLength > 0) {
payload = Arrays.copyOfRange(wholeMessage, KBSP_HEADER_LENGTH, payloadEnd);
} else {
payload = new byte[0];
}
final byte[] crcBytes = Arrays.copyOfRange(wholeMessage, payloadEnd, payloadEnd + 2);
final byte[] trailerBytes = Arrays.copyOfRange(wholeMessage, payloadEnd + 2, payloadEnd + 4);
if (!Arrays.equals(trailerBytes, KBSP_TRAILER_BYTES)) {
throw new KegboardMessageException("Illegal trailer value.");
}
final int expectedCrc = KegboardCrc.crc16Ccitt(wholeMessage, wholeMessage.length - 4) & 0x0ffff;
final int computedCrc = Shorts.fromBytes(crcBytes[1], crcBytes[0]) & 0x0ffff;
if (expectedCrc != computedCrc) {
throw new KegboardMessageException("Bad CRC: "
+ "expected=" + String.format("0x%04x ", Integer.valueOf(expectedCrc))
+ "computed=" + String.format("0x%04x ", Integer.valueOf(computedCrc)));
}
short messageType = Shorts.fromBytes(wholeMessage[9], wholeMessage[8]);
if (messageType != getMessageType()) {
throw new KegboardMessageException("Message type mismatch: expected=" + getMessageType()
+ " got=" + messageType);
}
// System.out.println(HexDump.dumpHexString(wholeMessage));
for (int i = 0; i <= (payload.length - 2); ) {
final int tagNum = payload[i] & 0x00ff;
final int length = payload[i + 1] & 0x00ff;
i += 2;
if ((i + length) <= payload.length) {
mTags.put(Integer.valueOf(tagNum),
Arrays.copyOfRange(payload, i, i + length));
}
i += length;
}
}
private static int getCrc(byte[] payload) {
byte[] message = Bytes.concat(KBSP_HEADER_BYTES, payload);
return KegboardCrc.crc16Ccitt(message, message.length);
}
public byte[] toBytes() {
byte[] payload = new byte[0];
for (Map.Entry<Integer, byte[]> entry : mTags.entrySet()) {
final byte[] tag = new byte[]{(byte) (entry.getKey().intValue() & 0x0ff)};
final byte[] value = entry.getValue();
final int len = value.length & 0x0ff;
final byte[] length = new byte[]{(byte) len};
payload = Bytes.concat(payload, tag, length, value);
}
byte[] messageType = new byte[]{(byte) (getMessageType() & 0x0ff), 00};
byte[] messageLength = new byte[]{(byte) (payload.length & 0x0ff), 00};
byte[] message = Bytes.concat(KBSP_HEADER_BYTES, messageType, messageLength, payload);
byte[] crc = Shorts.toByteArray((short) (getCrc(message) & 0x0ffff));
byte[] result = Bytes.concat(message, crc, KBSP_TRAILER_BYTES);
return result;
}
@Override
public String toString() {
Class<? extends KegboardMessage> clazz = this.getClass();
StringBuilder builder = new StringBuilder();
builder.append("<");
builder.append(clazz.getSimpleName());
builder.append(": ");
if (clazz == KegboardMessage.class) {
builder.append("type=");
builder.append(String.format("0x%04x ", Integer.valueOf(getMessageType())));
}
builder.append(getStringExtra());
builder.append(">");
return builder.toString();
}
protected String getStringExtra() {
return "";
}
public byte[] readTag(int tagNum) {
return mTags.get(Integer.valueOf(tagNum));
}
public int readTagAsShort(int tagNum) {
final byte[] tagData = readTag(tagNum);
if (tagData != null && tagData.length == 2) {
int result = (tagData[1] & 0xff) << 8;
result |= tagData[0] & 0xff;
return result;
}
return 0;
}
public Long readTagAsLong(int tagNum) {
final byte[] tagData = readTag(tagNum);
if (tagData != null && tagData.length == 4) {
long result = (tagData[3] & 0xff) << 24;
result |= (tagData[2] & 0xff) << 16;
result |= (tagData[1] & 0xff) << 8;
result |= tagData[0] & 0xff;
return Long.valueOf(result);
}
return null;
}
public String readTagAsString(int tagNum) {
final byte[] tagData = readTag(tagNum);
if (tagData == null) {
return null;
}
return new String(tagData).replace("\0", "");
}
private static int extractType(final byte[] bytes) {
return Shorts.fromBytes(bytes[9], bytes[8]);
}
public static KegboardMessage fromBytes(final byte[] bytes) throws KegboardMessageException {
if (bytes.length < KBSP_MIN_LENGTH) {
throw new KegboardMessageException("Too small: " + bytes.length);
} else if (bytes.length > KBSP_MAX_LENGTH) {
throw new KegboardMessageException("Too large: " + bytes.length);
}
final int messageType = extractType(bytes);
switch (messageType) {
case KegboardHelloMessage.MESSAGE_TYPE:
return new KegboardHelloMessage(bytes);
case KegboardMeterStatusMessage.MESSAGE_TYPE:
return new KegboardMeterStatusMessage(bytes);
case KegboardTemperatureReadingMessage.MESSAGE_TYPE:
return new KegboardTemperatureReadingMessage(bytes);
case KegboardOutputStatusMessage.MESSAGE_TYPE:
return new KegboardOutputStatusMessage(bytes);
case KegboardAuthTokenMessage.MESSAGE_TYPE:
return new KegboardAuthTokenMessage(bytes);
default:
throw new KegboardMessageException("Unknown message type");
}
}
public abstract short getMessageType();
}