1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 03:16:07 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vivomovehr/GfdiPacketParser.java
Mormegil 3a58314db6 Garmin Vivomove HR support
- communication protocols
- device support implementation
- download FIT file storage

Features:
- basic connectivity: time sync, battery status, HW/FW version info
- real-time activity tracking
- fitness data sync
- find the device, find the phone
- factory reset

Features implemented but not working:
- notifications: fully implemented, seem to communicate correctly, but not shown on watch

Features implemented partially (not expected to work now):
- weather information (and in future possibly weather alerts)
- music info
- firmware update: only the initial file upload implemented, not used

Things to improve/change:
- Device name hardcoded in `VivomoveHrCoordinator.getSupportedType`, service UUIDs not available
- Download FIT file storage: Should be store (and offer the user to export?) the FIT data forever?
- Obviously, various code improvements, cleanup, etc.
2023-07-20 20:30:14 +00:00

176 lines
6.2 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Parser of GFDI messages embedded in COBS packets.
* <p>
* COBS ensures there are no embedded NUL bytes inside the packet data, and wraps the message into NUL framing bytes.
*/
// Notes: not really optimized; does a lot of (re)allocation, might use more static buffers, I guess… And code cleanup as well.
public class GfdiPacketParser {
private static final Logger LOG = LoggerFactory.getLogger(GfdiPacketParser.class);
private static final long BUFFER_TIMEOUT = 1500L;
private static final byte[] EMPTY_BUFFER = new byte[0];
private static final byte[] BUFFER_FRAMING = new byte[1];
private byte[] buffer = EMPTY_BUFFER;
private byte[] packet;
private byte[] packetBuffer;
private int bufferPos;
private long lastUpdate;
private boolean insidePacket;
public void reset() {
buffer = EMPTY_BUFFER;
bufferPos = 0;
insidePacket = false;
packet = null;
packetBuffer = EMPTY_BUFFER;
}
public void receivedBytes(byte[] bytes) {
final long now = System.currentTimeMillis();
if ((now - lastUpdate) > BUFFER_TIMEOUT) {
reset();
}
lastUpdate = now;
final int bufferSize = buffer.length;
buffer = Arrays.copyOf(buffer, bufferSize + bytes.length);
System.arraycopy(bytes, 0, buffer, bufferSize, bytes.length);
parseBuffer();
}
public byte[] retrievePacket() {
final byte[] resultPacket = packet;
packet = null;
parseBuffer();
return resultPacket;
}
private void parseBuffer() {
if (packet != null) {
// packet is waiting, unable to parse more
return;
}
if (bufferPos >= buffer.length) {
// nothing to parse
return;
}
boolean startOfPacket = !insidePacket;
if (startOfPacket) {
byte b;
while (bufferPos < buffer.length && (b = buffer[bufferPos++]) != 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected non-zero byte while looking for framing: {}", Integer.toHexString(b));
}
}
if (bufferPos >= buffer.length) {
// nothing to parse
return;
}
insidePacket = true;
}
boolean endedWithFullChunk = false;
while (bufferPos < buffer.length) {
int chunkSize = -1;
int chunkStart = bufferPos;
int pos = bufferPos;
while (pos < buffer.length && ((chunkSize = (buffer[pos++] & 0xFF)) == 0) && startOfPacket) {
// skip repeating framing bytes (?)
bufferPos = pos;
chunkStart = pos;
}
if (startOfPacket && pos >= buffer.length) {
// incomplete framing, needs to wait for more data and try again
buffer = BUFFER_FRAMING;
bufferPos = 0;
insidePacket = false;
return;
}
assert chunkSize >= 0;
if (chunkSize == 0) {
// end of packet
// drop the last zero
if (endedWithFullChunk) {
// except when it was explicitly added (TODO: ugly, is it correct?)
packet = packetBuffer;
} else {
packet = Arrays.copyOf(packetBuffer, packetBuffer.length - 1);
}
packetBuffer = EMPTY_BUFFER;
insidePacket = false;
if (bufferPos == buffer.length - 1) {
buffer = EMPTY_BUFFER;
bufferPos = 0;
} else {
// TODO: Realloc buffer down
++bufferPos;
}
return;
}
if (chunkStart + chunkSize > buffer.length) {
// incomplete chunk, needs to wait for more data
return;
}
// completed chunk
final int packetPos = packetBuffer.length;
final int realChunkSize = chunkSize < 255 ? chunkSize : chunkSize - 1;
packetBuffer = Arrays.copyOf(packetBuffer, packetPos + realChunkSize);
System.arraycopy(buffer, chunkStart + 1, packetBuffer, packetPos, chunkSize - 1);
bufferPos = chunkStart + chunkSize;
endedWithFullChunk = chunkSize == 255;
startOfPacket = false;
}
}
public static byte[] wrapMessageToPacket(byte[] message) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(message.length + 2 + (message.length + 253) / 254)) {
outputStream.write(0);
int chunkStart = 0;
for (int i = 0; i < message.length; ++i) {
if (message[i] == 0) {
chunkStart = appendChunk(message, outputStream, chunkStart, i);
}
}
if (chunkStart <= message.length) {
appendChunk(message, outputStream, chunkStart, message.length);
}
outputStream.write(0);
return outputStream.toByteArray();
} catch (IOException e) {
LOG.error("Error writing to memory buffer", e);
throw new RuntimeException(e);
}
}
private static int appendChunk(byte[] message, ByteArrayOutputStream outputStream, int chunkStart, int messagePos) {
int chunkLength = messagePos - chunkStart;
while (true) {
if (chunkLength >= 255) {
// write 255-byte chunk
outputStream.write(255);
outputStream.write(message, chunkStart, 254);
chunkLength -= 254;
chunkStart += 254;
} else {
// write chunk from chunkStart to here
outputStream.write(chunkLength + 1);
outputStream.write(message, chunkStart, chunkLength);
chunkStart = messagePos + 1;
break;
}
}
return chunkStart;
}
}