234 lines
9.2 KiB
Java
234 lines
9.2 KiB
Java
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.lang.reflect.Method;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public abstract class GFDIMessage {
|
|
public static final int MESSAGE_REQUEST = 5001;
|
|
public static final int MESSAGE_DIRECTORY_FILE_FILTER_REQUEST = 5007;
|
|
public static final int MESSAGE_FILE_READY = 5009;
|
|
public static final int MESSAGE_BATTERY_STATUS = 5023;
|
|
public static final int MESSAGE_NOTIFICATION_SOURCE = 5033;
|
|
public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034;
|
|
public static final int MESSAGE_GNCS_DATA_SOURCE = 5035;
|
|
public static final int MESSAGE_NOTIFICATION_SERVICE_SUBSCRIPTION = 5036;
|
|
public static final int MESSAGE_SYNC_REQUEST = 5037;
|
|
public static final int MESSAGE_AUTH_NEGOTIATION = 5101;
|
|
protected static final Logger LOG = LoggerFactory.getLogger(GFDIMessage.class);
|
|
private static int maxPacketSize = 375; //safe default?
|
|
protected final ByteBuffer response = ByteBuffer.allocate(1000);
|
|
protected GFDIStatusMessage statusMessage;
|
|
protected GarminMessage garminMessage;
|
|
|
|
public static int getMaxPacketSize() {
|
|
return maxPacketSize;
|
|
}
|
|
|
|
public static void setMaxPacketSize(int maxPacketSize) {
|
|
GFDIMessage.maxPacketSize = maxPacketSize;
|
|
}
|
|
|
|
public static GFDIMessage parseIncoming(byte[] message) {
|
|
final MessageReader messageReader = new MessageReader(message);
|
|
|
|
int messageType = messageReader.readShort();
|
|
if (messageType > 0x8000) {
|
|
messageType = (messageType & 0xff) + 5000;
|
|
}
|
|
try {
|
|
final GarminMessage garminMessage = GarminMessage.fromId(messageType);
|
|
if (garminMessage == null) {
|
|
LOG.warn("Unknown message type {}, message {}", messageType, message);
|
|
return new UnhandledMessage(messageType);
|
|
}
|
|
final Method m = garminMessage.objectClass.getMethod("parseIncoming", MessageReader.class, GarminMessage.class);
|
|
return garminMessage.objectClass.cast(m.invoke(null, messageReader, garminMessage));
|
|
} catch (final Exception e) {
|
|
LOG.error("UNHANDLED GFDI MESSAGE TYPE {}, MESSAGE {}", messageType, message, e);
|
|
return new UnhandledMessage(messageType);
|
|
} finally {
|
|
messageReader.warnIfLeftover(messageType);
|
|
}
|
|
}
|
|
|
|
protected abstract boolean generateOutgoing();
|
|
|
|
public byte[] getOutgoingMessage() {
|
|
response.clear();
|
|
boolean toSend = generateOutgoing();
|
|
response.order(ByteOrder.LITTLE_ENDIAN);
|
|
if (!toSend)
|
|
return null;
|
|
addLengthAndChecksum();
|
|
response.flip();
|
|
|
|
final byte[] packet = new byte[response.limit()];
|
|
response.get(packet);
|
|
return packet;
|
|
}
|
|
|
|
protected GFDIStatusMessage getStatusMessage() {
|
|
return new GenericStatusMessage(garminMessage, Status.ACK);
|
|
}
|
|
|
|
public List<GBDeviceEvent> getGBDeviceEvent() {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
public byte[] getAckBytestream() {
|
|
if (null == this.statusMessage) {
|
|
return null;
|
|
}
|
|
return this.statusMessage.getOutgoingMessage();
|
|
}
|
|
|
|
private void addLengthAndChecksum() {
|
|
response.putShort(0, (short) (response.position() + 2));
|
|
response.putShort((short) ChecksumCalculator.computeCrc(response.asReadOnlyBuffer(), 0, response.position()));
|
|
}
|
|
|
|
public enum GarminMessage {
|
|
RESPONSE(5000, GFDIStatusMessage.class), //TODO: STATUS is a better name?
|
|
DOWNLOAD_REQUEST(5002, DownloadRequestMessage.class),
|
|
UPLOAD_REQUEST(5003, UploadRequestMessage.class),
|
|
FILE_TRANSFER_DATA(5004, FileTransferDataMessage.class),
|
|
CREATE_FILE(5005, CreateFileMessage.class),
|
|
SET_FILE_FLAG(5008, SetFileFlagsMessage.class),
|
|
FIT_DEFINITION(5011, FitDefinitionMessage.class),
|
|
FIT_DATA(5012, FitDataMessage.class),
|
|
WEATHER_REQUEST(5014, WeatherMessage.class),
|
|
DEVICE_INFORMATION(5024, DeviceInformationMessage.class),
|
|
DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class),
|
|
SYSTEM_EVENT(5030, SystemEventMessage.class),
|
|
SUPPORTED_FILE_TYPES_REQUEST(5031, SupportedFileTypesMessage.class),
|
|
NOTIFICATION_UPDATE(5033, NotificationUpdateMessage.class),
|
|
NOTIFICATION_CONTROL(5034, NotificationControlMessage.class),
|
|
NOTIFICATION_DATA(5035, NotificationDataMessage.class),
|
|
NOTIFICATION_SUBSCRIPTION(5036, NotificationSubscriptionMessage.class),
|
|
FIND_MY_PHONE_REQUEST(5039, FindMyPhoneRequestMessage.class),
|
|
FIND_MY_PHONE_CANCEL(5040, FindMyPhoneCancelMessage.class),
|
|
MUSIC_CONTROL(5041, MusicControlMessage.class),
|
|
MUSIC_CONTROL_CAPABILITIES(5042, MusicControlCapabilitiesMessage.class),
|
|
PROTOBUF_REQUEST(5043, ProtobufMessage.class),
|
|
PROTOBUF_RESPONSE(5044, ProtobufMessage.class),
|
|
MUSIC_CONTROL_ENTITY_UPDATE(5049, MusicControlEntityUpdateMessage.class),
|
|
CONFIGURATION(5050, ConfigurationMessage.class),
|
|
CURRENT_TIME_REQUEST(5052, CurrentTimeRequestMessage.class),
|
|
;
|
|
private final Class<? extends GFDIMessage> objectClass;
|
|
private final int id;
|
|
|
|
GarminMessage(int id, Class<? extends GFDIMessage> objectClass) {
|
|
this.id = id;
|
|
this.objectClass = objectClass;
|
|
}
|
|
|
|
public static Class<? extends GFDIMessage> getClassFromId(final int id) {
|
|
for (final GarminMessage garminMessage : GarminMessage.values()) {
|
|
if (garminMessage.getId() == id) {
|
|
return garminMessage.getObjectClass();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Nullable
|
|
public static GarminMessage fromId(final int id) {
|
|
for (final GarminMessage garminMessage : GarminMessage.values()) {
|
|
if (garminMessage.getId() == id) {
|
|
return garminMessage;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public int getId() {
|
|
return id;
|
|
}
|
|
|
|
private Class<? extends GFDIMessage> getObjectClass() {
|
|
return objectClass;
|
|
}
|
|
}
|
|
|
|
public enum Status {
|
|
ACK,
|
|
NAK,
|
|
UNSUPPORTED,
|
|
DECODE_ERROR,
|
|
CRC_ERROR,
|
|
LENGTH_ERROR;
|
|
|
|
public static Status fromCode(final int code) {
|
|
for (final Status status : Status.values()) {
|
|
if (status.ordinal() == code) {
|
|
return status;
|
|
}
|
|
}
|
|
throw new IllegalArgumentException("Unknown status code " + code);
|
|
}
|
|
|
|
}
|
|
|
|
public static class MessageReader extends GarminByteBufferReader {
|
|
|
|
private final int payloadSize;
|
|
|
|
public MessageReader(byte[] data) {
|
|
super(data);
|
|
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
this.payloadSize = readShort(); //includes CRC
|
|
checkSize();
|
|
checkCRC();
|
|
this.byteBuffer.limit(payloadSize - 2); //remove CRC
|
|
}
|
|
|
|
public void skip(int offset) {
|
|
if (remaining() < offset) throw new IllegalStateException();
|
|
byteBuffer.position(byteBuffer.position() + offset);
|
|
}
|
|
|
|
private void checkSize() {
|
|
if (payloadSize != byteBuffer.capacity()) {
|
|
LOG.error("Received GFDI packet with invalid length: {} vs {}", payloadSize, byteBuffer.capacity());
|
|
throw new IllegalArgumentException("Received GFDI packet with invalid length");
|
|
}
|
|
}
|
|
|
|
private void checkCRC() {
|
|
final int crc = Short.toUnsignedInt(byteBuffer.getShort(payloadSize - 2));
|
|
final int correctCrc = ChecksumCalculator.computeCrc(byteBuffer.asReadOnlyBuffer(), 0, payloadSize - 2);
|
|
if (crc != correctCrc) {
|
|
LOG.error("Received GFDI packet with invalid CRC: {} vs {}", crc, correctCrc);
|
|
throw new IllegalArgumentException("Received GFDI packet with invalid CRC");
|
|
}
|
|
}
|
|
|
|
public void warnIfLeftover(int messageType) {
|
|
if (byteBuffer.hasRemaining() && byteBuffer.position() < (byteBuffer.limit())) {
|
|
int pos = byteBuffer.position();
|
|
int numBytes = (byteBuffer.limit()) - byteBuffer.position();
|
|
byte[] leftover = new byte[numBytes];
|
|
byteBuffer.get(leftover);
|
|
byteBuffer.position(pos);
|
|
LOG.warn("Leftover bytes when parsing message type {}. Bytes: {}, complete message: {}", messageType, GB.hexdump(leftover), GB.hexdump(byteBuffer.array()));
|
|
}
|
|
}
|
|
}
|
|
}
|