/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWIM_STYLE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_END;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_START;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_UNIX_EPOCH_SECONDS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiSimpleActivityParser {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSimpleActivityParser.class);
public static final String XIAOMI_WORKOUT_TYPE = "xiaomiWorkoutType";
private final int headerSize;
private final List dataEntries;
public XiaomiSimpleActivityParser(final int headerSize, final List dataEntries) {
this.headerSize = headerSize;
this.dataEntries = dataEntries;
}
public void parse(final BaseActivitySummary summary, final ByteBuffer buf) {
final ActivitySummaryData summaryData = new ActivitySummaryData();
final byte[] header = new byte[headerSize];
buf.get(header);
LOG.debug("Header: {}", GB.hexdump(header));
for (int i = 0; i < dataEntries.size(); i++) {
final XiaomiSimpleDataEntry dataEntry = dataEntries.get(i);
final Number value = dataEntry.get(buf);
if (value == null) {
LOG.debug("Skipping unknown field {}", i);
continue;
}
// Each bit in the header marks whether the data is valid or not, in order of the fields
final boolean validData = (header[i / 8] & (1 << (7 - (i % 8)))) != 0;
// FIXME: We can't use the header before identifying the correct field lenggths for unknown fields
// or parsing gets out of sync with the header and we will potentially ignore valid data
//if (!validData) {
// LOG.debug("Ignoring non-valid data {}", i);
// continue;
//}
if (dataEntry.getKey().equals(TIME_END)) {
if (dataEntry.getUnit().equals(UNIT_UNIX_EPOCH_SECONDS)) {
summary.setEndTime(new Date(value.intValue() * 1000L));
} else {
throw new IllegalArgumentException("endTime should be an unix epoch");
}
} else if (dataEntry.getKey().equals(TIME_START)) {
// ignored
} else if (dataEntry.getKey().equals(SWIM_STYLE)) {
String swimStyleName = "unknown";
final float swimStyle = value.floatValue();
if (swimStyle == 0) {
swimStyleName = "medley";
} else if (swimStyle == 1) {
swimStyleName = "breaststroke";
} else if (swimStyle == 2) {
swimStyleName = "freestyle";
} else if (swimStyle == 3) {
swimStyleName = "backstroke";
} else if (swimStyle == 4) {
swimStyleName = "butterfly";
}
summaryData.add(dataEntry.getKey(), swimStyleName);
} else if (dataEntry.getKey().equals(XIAOMI_WORKOUT_TYPE)) {
switch (value.intValue()) {
case 1:
summary.setActivityKind(ActivityKind.OUTDOOR_RUNNING.getCode());
break;
case 2:
summary.setActivityKind(ActivityKind.WALKING.getCode());
break;
case 4:
summary.setActivityKind(ActivityKind.TREKKING.getCode());
break;
case 5:
summary.setActivityKind(ActivityKind.TRAIL_RUN.getCode());
break;
case 6:
summary.setActivityKind(ActivityKind.OUTDOOR_CYCLING.getCode());
break;
case 7: // indoor cycling 0x0007
summary.setActivityKind(ActivityKind.INDOOR_CYCLING.getCode());
break;
case 8: // freestyle 0x0008
summary.setActivityKind(ActivityKind.FREE_TRAINING.getCode());
break;
case 12: // yoga 0x000c
summary.setActivityKind(ActivityKind.YOGA.getCode());
break;
case 15:
summary.setActivityKind(ActivityKind.OUTDOOR_WALKING.getCode());
break;
case 16: // HIIT 0x0010
summary.setActivityKind(ActivityKind.HIIT.getCode());
break;
case 201: // skateboard 0x00c9
summary.setActivityKind(ActivityKind.SKATEBOARDING.getCode());
break;
case 202: // roller skating 0x00ca
summary.setActivityKind(ActivityKind.ROLLER_SKATING.getCode());
break;
case 301: // stair climbing 0x012d
summary.setActivityKind(ActivityKind.STAIRS.getCode());
break;
case 303: // core training 0x012f
summary.setActivityKind(ActivityKind.CORE_TRAINING.getCode());
break;
case 304: // flexibility 0x0130
summary.setActivityKind(ActivityKind.FLEXIBILITY.getCode());
break;
case 305: // pilates 0x0131
summary.setActivityKind(ActivityKind.PILATES.getCode());
break;
case 307: // stretching 0x0133
summary.setActivityKind(ActivityKind.STRETCHING.getCode());
break;
case 308: // strength 0x0134
summary.setActivityKind(ActivityKind.STRENGTH_TRAINING.getCode());
break;
case 310: // aerobics 0x0136
summary.setActivityKind(ActivityKind.AEROBICS.getCode());
break;
case 399: // indoor-Fitness 0x018f
summary.setActivityKind(ActivityKind.INDOOR_FITNESS.getCode());
break;
case 499: // dancing 0x01f3
summary.setActivityKind(ActivityKind.DANCE.getCode());
break;
case 600: // Soccer 0x0258
summary.setActivityKind(ActivityKind.SOCCER.getCode());
break;
case 601: // basketball 0x0259
summary.setActivityKind(ActivityKind.BASKETBALL.getCode());
break;
case 607: // table tennis 0x025f
summary.setActivityKind(ActivityKind.TABLE_TENNIS.getCode());
break;
case 608: // badminton 0x0260
summary.setActivityKind(ActivityKind.BADMINTON.getCode());
break;
case 609: // tennis 0x0261
summary.setActivityKind(ActivityKind.TENNIS.getCode());
break;
case 614: // billiard 0x0266
summary.setActivityKind(ActivityKind.BILLIARDS.getCode());
break;
case 619: // golf 0x026b
summary.setActivityKind(ActivityKind.GOLF.getCode());
break;
case 700: // ice skating 0x02bc
summary.setActivityKind(ActivityKind.ICE_SKATING.getCode());
break;
case 708: // snowboard 0x02c4
summary.setActivityKind(ActivityKind.SNOWBOARDING.getCode());
break;
case 709: // skiing 0x02c5
summary.setActivityKind(ActivityKind.SKIING.getCode());
break;
case 808: // shuttlecock 0x0328
summary.setActivityKind(ActivityKind.SHUTTLECOCK.getCode());
break;
default:
summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
}
} else {
summaryData.add(dataEntry.getKey(), value.floatValue(), dataEntry.getUnit());
}
}
summary.setSummaryData(summaryData.toString());
}
public static class Builder {
private int headerSize;
private List dataEntries = new ArrayList<>();
public Builder setHeaderSize(final int headerSize) {
this.headerSize = headerSize;
return this;
}
public Builder addByte(final String key, final String unit) {
dataEntries.add(new XiaomiSimpleDataEntry(key, unit, buf -> buf.get() & 0xff));
return this;
}
public Builder addShort(final String key, final String unit) {
dataEntries.add(new XiaomiSimpleDataEntry(key, unit, ByteBuffer::getShort));
return this;
}
public Builder addInt(final String key, final String unit) {
dataEntries.add(new XiaomiSimpleDataEntry(key, unit, ByteBuffer::getInt));
return this;
}
public Builder addFloat(final String key, final String unit) {
dataEntries.add(new XiaomiSimpleDataEntry(key, unit, ByteBuffer::getFloat));
return this;
}
public Builder addUnknown(final int sizeBytes) {
dataEntries.add(new XiaomiSimpleDataEntry(null, null, buf -> {
for (int i = 0; i < sizeBytes; i++) {
buf.get();
}
return null;
}));
return this;
}
public XiaomiSimpleActivityParser build() {
return new XiaomiSimpleActivityParser(headerSize, dataEntries);
}
}
}