Moyoung: Handle stress packets from Rainbuvvy T97, Colmi V89 and Advan SE1

This commit is contained in:
Arjan Schrijver
2025-06-11 20:58:00 +02:00
parent 0d187224dd
commit 4e1cb5fbc9
8 changed files with 186 additions and 1 deletions
@@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumDeviceVersion;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumMetricSystem;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumTimeSystem;
@@ -57,10 +58,12 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung.MoyoungDeviceSupport;
@@ -98,6 +101,8 @@ public abstract class AbstractMoyoungDeviceCoordinator extends AbstractBLEDevice
qb.where(MoyoungSpo2SampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getMoyoungBloodPressureSampleDao().queryBuilder();
qb.where(MoyoungBloodPressureSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getMoyoungStressSampleDao().queryBuilder();
qb.where(MoyoungStressSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
@@ -125,6 +130,11 @@ public abstract class AbstractMoyoungDeviceCoordinator extends AbstractBLEDevice
return new MoyoungSpo2SampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session) {
return new MoyoungStressSampleProvider(device, session);
}
@Override
public int getAlarmSlotCount(GBDevice device) {
return 3;
@@ -80,4 +80,9 @@ public class AdvanWatchSE1AICoordinator extends AbstractMoyoungDeviceCoordinator
public boolean supportsRemSleep() {
return true;
}
@Override
public boolean supportsStressMeasurement() {
return true;
}
}
@@ -80,4 +80,9 @@ public class ColmiV89Coordinator extends AbstractMoyoungDeviceCoordinator {
public boolean supportsRemSleep() {
return true;
}
@Override
public boolean supportsStressMeasurement() {
return true;
}
}
@@ -234,6 +234,7 @@ public class MoyoungConstants {
public static final byte ARG_ADVANCED_SET_ALARM = 0x05;
public static final byte ARG_ADVANCED_SET_CALENDAR = 0x08;
public static final byte ARG_ADVANCED_QUERY_STOCKS = 0x0e;
public static final byte ARG_ADVANCED_STRESS_PACKET = 0x11;
public static final byte ARG_ADVANCED_QUERY_ALARMS = 0x15;
public static final byte ARG_ALARM_SET = 0x00;
@@ -85,4 +85,9 @@ public class RainbuvvyT97Coordinator extends AbstractMoyoungDeviceCoordinator {
public boolean supportsRemSleep() {
return true;
}
@Override
public boolean supportsStressMeasurement() {
return true;
}
}
@@ -0,0 +1,56 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungStressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MoyoungStressSampleProvider extends AbstractTimeSampleProvider<MoyoungStressSample> {
public MoyoungStressSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<MoyoungStressSample, ?> getSampleDao() {
return getSession().getMoyoungStressSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungStressSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungStressSampleDao.Properties.DeviceId;
}
@Override
public MoyoungStressSample createSample() {
return new MoyoungStressSample();
}
}
@@ -75,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungBlood
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSleepStageSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumDeviceVersion;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumLanguage;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumMetricSystem;
@@ -91,6 +92,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungStressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -488,6 +490,12 @@ public class MoyoungDeviceSupport extends AbstractBTLEDeviceSupport {
return true;
}
if (packetType == MoyoungConstants.CMD_ADVANCED_QUERY && payload[0] == MoyoungConstants.ARG_ADVANCED_STRESS_PACKET)
{
handleStressPacket(payload);
return true;
}
if (packetType == MoyoungConstants.CMD_QUERY_POWER_SAVING)
{
LOG.info("Power saving set to: {}", payload[0] == 0x01);
@@ -667,6 +675,85 @@ public class MoyoungDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
private void handleStressPacket(byte[] payload) {
byte packetType = payload[1];
switch (packetType) {
case 0x00:
// Single stress measurement result
int stressLevel = payload[2] & 0xff;
try (DBHandler dbHandler = GBApplication.acquireDB()) {
MoyoungStressSampleProvider sampleProvider = new MoyoungStressSampleProvider(getDevice(), dbHandler.getDaoSession());
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
MoyoungStressSample sample = new MoyoungStressSample();
sample.setTimestamp(System.currentTimeMillis());
sample.setStress(stressLevel);
sample.setDeviceId(deviceId);
sample.setUserId(userId);
sampleProvider.addSample(sample);
} catch (Exception e) {
LOG.error("Error acquiring database for recording stress samples", e);
}
break;
case 0x01:
// Unknown
break;
case 0x02:
// Unknown
break;
case 0x03:
// Day stress measurements sync result
byte daysAgo = payload[2];
ArrayList<MoyoungStressSample> stressSamples = new ArrayList<>();
Calendar sampleCal = Calendar.getInstance();
sampleCal.add(Calendar.DAY_OF_MONTH, -daysAgo);
sampleCal.set(Calendar.SECOND, 0);
sampleCal.set(Calendar.MILLISECOND, 0);
int startIndex = 3;
for (int i = 0; i < 26; i++) {
if (payload[startIndex + i] != 0x00) {
// Determine time of day
sampleCal.set(Calendar.HOUR_OF_DAY, Math.floorDiv(i, 2));
if (i % 2 == 0) {
sampleCal.set(Calendar.MINUTE, 0);
} else {
sampleCal.set(Calendar.MINUTE, 30);
}
if (sampleCal.after(Calendar.getInstance())) {
continue;
}
LOG.info("Stress level is {} at {}", payload[startIndex + i] & 0xff, sampleCal.getTime());
// Build sample object and save in database
MoyoungStressSample gbSample = new MoyoungStressSample();
gbSample.setTimestamp(sampleCal.getTimeInMillis());
gbSample.setStress(payload[startIndex + i] & 0xff);
stressSamples.add(gbSample);
}
}
if (!stressSamples.isEmpty()) {
try (DBHandler db = GBApplication.acquireDB()) {
MoyoungStressSampleProvider sampleProvider = new MoyoungStressSampleProvider(getDevice(), db.getDaoSession());
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
for (final MoyoungStressSample sample : stressSamples) {
sample.setDeviceId(deviceId);
sample.setUserId(userId);
}
LOG.info("Will persist {} stress samples", stressSamples.size());
sampleProvider.addSamples(stressSamples);
} catch (Exception e) {
LOG.error("Error acquiring database for recording stress samples", e);
}
}
break;
case 0x04:
// Unknown
break;
}
}
private void handleGetAlarmsResponse(byte[] payload)
{
boolean payloadV2 = false;
@@ -1031,6 +1118,14 @@ public class MoyoungDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.error("Error fetching workouts: ", e);
}
}
try {
TransactionBuilder builder = performInitialized("fetchStress");
sendPacket(builder, MoyoungPacketOut.buildPacket(getMtu(), MoyoungConstants.CMD_ADVANCED_QUERY, new byte[] { MoyoungConstants.ARG_ADVANCED_STRESS_PACKET, 0x03, 0x00 }));
sendPacket(builder, MoyoungPacketOut.buildPacket(getMtu(), MoyoungConstants.CMD_ADVANCED_QUERY, new byte[] { MoyoungConstants.ARG_ADVANCED_STRESS_PACKET, 0x03, 0x01 }));
builder.queue(getQueue());
} catch (IOException e) {
LOG.error("Error while sending stress sync request: ", e);
}
}
private final Runnable updateIdleStepsRunnable = new Runnable() {