package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator; import java.nio.ByteBuffer; public class CobsCoDec { private static final long BUFFER_TIMEOUT = 1500L; // turn this value up while debugging private final ByteBuffer byteBuffer = ByteBuffer.allocate(10_000); private long lastUpdate; private byte[] cobsDecodedMessage; /** * Accumulates received bytes in a local buffer, clearing it after a timeout, and attempts to * parse it. * * @param bytes */ public void receivedBytes(byte[] bytes) { final long now = System.currentTimeMillis(); if ((now - lastUpdate) > BUFFER_TIMEOUT) { reset(); } lastUpdate = now; byteBuffer.put(bytes); decode(); } private void reset() { cobsDecodedMessage = null; byteBuffer.clear(); } public byte[] retrieveMessage() { final byte[] resultPacket = cobsDecodedMessage; cobsDecodedMessage = null; return resultPacket; } /** * COBS decoding algorithm variant, which relies on a leading and a trailing 0 byte (the former * is not part of default implementations). * This function removes the complete message from the internal buffer, if it could be decoded. */ private void decode() { if (cobsDecodedMessage != null) { // packet is waiting, unable to parse more return; } if (byteBuffer.position() < 4) { // minimal payload length including the padding return; } if (0 != byteBuffer.get(byteBuffer.position() - 1)) return; //no 0x00 at the end, hence no full packet byteBuffer.position(byteBuffer.position() - 1); //don't process the trailing 0 byteBuffer.flip(); if (0 != byteBuffer.get()) return; //no 0x00 at the start ByteBuffer decodedBytesBuffer = ByteBuffer.allocate(byteBuffer.limit()); //leading and trailing 0x00 bytes while (byteBuffer.hasRemaining()) { byte code = byteBuffer.get(); if (code == 0) { break; } int codeValue = code & 0xFF; int payloadSize = codeValue - 1; for (int i = 0; i < payloadSize; i++) { decodedBytesBuffer.put(byteBuffer.get()); } if (codeValue != 0xFF && byteBuffer.hasRemaining()) { decodedBytesBuffer.put((byte) 0); // Append a zero byte after the payload } } decodedBytesBuffer.flip(); cobsDecodedMessage = new byte[decodedBytesBuffer.remaining()]; decodedBytesBuffer.get(cobsDecodedMessage); byteBuffer.compact(); } // this implementation of COBS relies on a leading and a trailing 0 byte (the former is not part of default implementations) public byte[] encode(byte[] data) { ByteBuffer encodedBytesBuffer = ByteBuffer.allocate((data.length * 2) + 1); // Maximum expansion encodedBytesBuffer.put((byte) 0);// Garmin initial padding ByteBuffer buffer = ByteBuffer.wrap(data); while (buffer.position() < buffer.limit()) { int startPos = buffer.position(); int zeroIndex = buffer.position(); while (buffer.hasRemaining() && buffer.get() != 0) { zeroIndex++; } int payloadSize = zeroIndex - startPos; while (payloadSize >= 0xFE) { encodedBytesBuffer.put((byte) 0xFF); // Maximum payload size indicator encodedBytesBuffer.put(data, startPos, 0xFE); payloadSize -= 0xFE; startPos += 0xFE; } encodedBytesBuffer.put((byte) (payloadSize + 1)); encodedBytesBuffer.put(data, startPos, payloadSize); if (buffer.hasRemaining()) { zeroIndex++; // Include the zero byte in the next block } if (!buffer.hasRemaining() && payloadSize == 0) { break; } buffer.position(zeroIndex); } encodedBytesBuffer.put((byte) 0); // Append a zero byte to indicate end of encoding encodedBytesBuffer.flip(); byte[] encodedBytes = new byte[encodedBytesBuffer.remaining()]; encodedBytesBuffer.get(encodedBytes); return encodedBytes; } }