mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Support for Femometer Vinca 2 and HealthThermometerProfile (#3369)
Co-authored-by: ahormann <ahormann@gmx.net> Co-committed-by: ahormann <ahormann@gmx.net>
This commit is contained in:
parent
11de66f8e4
commit
28e673415f
@ -38,12 +38,14 @@ public class GBDaoGenerator {
|
|||||||
private static final String SAMPLE_STEPS = "steps";
|
private static final String SAMPLE_STEPS = "steps";
|
||||||
private static final String SAMPLE_RAW_KIND = "rawKind";
|
private static final String SAMPLE_RAW_KIND = "rawKind";
|
||||||
private static final String SAMPLE_HEART_RATE = "heartRate";
|
private static final String SAMPLE_HEART_RATE = "heartRate";
|
||||||
|
private static final String SAMPLE_TEMPERATURE = "temperature";
|
||||||
|
private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType";
|
||||||
private static final String TIMESTAMP_FROM = "timestampFrom";
|
private static final String TIMESTAMP_FROM = "timestampFrom";
|
||||||
private static final String TIMESTAMP_TO = "timestampTo";
|
private static final String TIMESTAMP_TO = "timestampTo";
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
final Schema schema = new Schema(59, MAIN_PACKAGE + ".entities");
|
final Schema schema = new Schema(60, MAIN_PACKAGE + ".entities");
|
||||||
|
|
||||||
Entity userAttributes = addUserAttributes(schema);
|
Entity userAttributes = addUserAttributes(schema);
|
||||||
Entity user = addUserInfo(schema, userAttributes);
|
Entity user = addUserInfo(schema, userAttributes);
|
||||||
@ -101,6 +103,7 @@ public class GBDaoGenerator {
|
|||||||
addWena3HeartRateSample(schema, user, device);
|
addWena3HeartRateSample(schema, user, device);
|
||||||
addWena3Vo2Sample(schema, user, device);
|
addWena3Vo2Sample(schema, user, device);
|
||||||
addWena3StressSample(schema, user, device);
|
addWena3StressSample(schema, user, device);
|
||||||
|
addFemometerVinca2TemperatureSample(schema, user, device);
|
||||||
|
|
||||||
addCalendarSyncState(schema, device);
|
addCalendarSyncState(schema, device);
|
||||||
addAlarms(schema, user, device);
|
addAlarms(schema, user, device);
|
||||||
@ -938,4 +941,17 @@ public class GBDaoGenerator {
|
|||||||
perAppSetting.addStringProperty("vibrationRepetition");
|
perAppSetting.addStringProperty("vibrationRepetition");
|
||||||
return perAppSetting;
|
return perAppSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addTemperatureProperties(Entity activitySample) {
|
||||||
|
activitySample.addFloatProperty(SAMPLE_TEMPERATURE).notNull().codeBeforeGetter(OVERRIDE);
|
||||||
|
activitySample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull().codeBeforeGetter(OVERRIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Entity addFemometerVinca2TemperatureSample(Schema schema, Entity user, Entity device) {
|
||||||
|
Entity sample = addEntity(schema, "FemometerVinca2TemperatureSample");
|
||||||
|
addCommonTimeSampleProperties("AbstractTemperatureSample", sample, user, device);
|
||||||
|
addTemperatureProperties(sample);
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -362,4 +362,5 @@ public class DeviceSettingsPreferenceConst {
|
|||||||
public static final String PREF_VOICE_SERVICE_LANGUAGE = "voice_service_language";
|
public static final String PREF_VOICE_SERVICE_LANGUAGE = "voice_service_language";
|
||||||
|
|
||||||
public static final String PREF_TEMPERATURE_SCALE_CF = "temperature_scale_cf";
|
public static final String PREF_TEMPERATURE_SCALE_CF = "temperature_scale_cf";
|
||||||
|
public static final String PREF_FEMOMETER_MEASUREMENT_MODE = "femometer_measurement_mode";
|
||||||
}
|
}
|
||||||
|
@ -506,6 +506,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||||||
addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_FOCUS_ON_VOICE);
|
addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_FOCUS_ON_VOICE);
|
||||||
addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_TIMEOUT);
|
addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_TIMEOUT);
|
||||||
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
|
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
|
||||||
|
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
|
||||||
|
|
||||||
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);
|
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);
|
||||||
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);
|
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);
|
||||||
|
@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
@ -190,6 +191,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||||
|
|
||||||
@ -235,6 +236,11 @@ public interface DeviceCoordinator {
|
|||||||
*/
|
*/
|
||||||
TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session);
|
TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sample provider for temperature data, for the device being supported.
|
||||||
|
*/
|
||||||
|
TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the sample provider for SpO2 data, for the device being supported.
|
* Returns the sample provider for SpO2 data, for the device being supported.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.femometer;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import de.greenrobot.dao.query.QueryBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSampleDao;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.femometer.FemometerVinca2DeviceSupport;
|
||||||
|
|
||||||
|
public class FemometerVinca2DeviceCoordinator extends AbstractDeviceCoordinator {
|
||||||
|
@Override
|
||||||
|
public String getManufacturer() {
|
||||||
|
return "Joytech Healthcare";
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||||
|
return FemometerVinca2DeviceSupport.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeSampleProvider<FemometerVinca2TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
return new FemometerVinca2SampleProvider(device, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Pattern getSupportedDeviceName() {
|
||||||
|
return Pattern.compile("BM-Vinca2");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDeviceNameResource() {
|
||||||
|
return R.string.devicetype_femometer_vinca2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDefaultIconResource() {
|
||||||
|
return R.drawable.ic_device_thermometer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDisabledIconResource() {
|
||||||
|
return R.drawable.ic_device_thermometer_disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||||
|
Long deviceId = device.getId();
|
||||||
|
QueryBuilder<?> qb = session.getFemometerVinca2TemperatureSampleDao().queryBuilder();
|
||||||
|
qb.where(FemometerVinca2TemperatureSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBondingStyle(){
|
||||||
|
return BONDING_STYLE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getPairingActivity() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAlarmSlotCount(final GBDevice device) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||||
|
return new int[]{
|
||||||
|
R.xml.devicesettings_volume,
|
||||||
|
R.xml.devicesettings_femometer,
|
||||||
|
R.xml.devicesettings_temperature_scale_cf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.femometer;
|
||||||
|
|
||||||
|
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.DaoSession;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSampleDao;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
|
public class FemometerVinca2SampleProvider extends AbstractTimeSampleProvider<FemometerVinca2TemperatureSample> {
|
||||||
|
|
||||||
|
public FemometerVinca2SampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
super(device, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public AbstractDao<FemometerVinca2TemperatureSample, ?> getSampleDao() {
|
||||||
|
return getSession().getFemometerVinca2TemperatureSampleDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected Property getTimestampSampleProperty() {
|
||||||
|
return FemometerVinca2TemperatureSampleDao.Properties.Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected Property getDeviceIdentifierSampleProperty() {
|
||||||
|
return FemometerVinca2TemperatureSampleDao.Properties.DeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FemometerVinca2TemperatureSample createSample() {
|
||||||
|
return new FemometerVinca2TemperatureSample();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.entities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
|
|
||||||
|
public abstract class AbstractTemperatureSample extends AbstractTimeSample implements TemperatureSample {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getClass().getSimpleName() + "{" +
|
||||||
|
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) +
|
||||||
|
", temperature=" + getTemperature() +
|
||||||
|
", temperatureType=" + getTemperatureType() +
|
||||||
|
", userId=" + getUserId() +
|
||||||
|
", deviceId=" + getDeviceId() +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100Devi
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
|
||||||
@ -265,6 +266,8 @@ public enum DeviceType {
|
|||||||
SOFLOW_SO6(550, SoFlowCoordinator.class),
|
SOFLOW_SO6(550, SoFlowCoordinator.class),
|
||||||
WITHINGS_STEEL_HR(560, WithingsSteelHRDeviceCoordinator.class),
|
WITHINGS_STEEL_HR(560, WithingsSteelHRDeviceCoordinator.class),
|
||||||
SONY_WENA_3(570, SonyWena3Coordinator.class),
|
SONY_WENA_3(570, SonyWena3Coordinator.class),
|
||||||
|
|
||||||
|
FEMOMETER_VINCA2(580, FemometerVinca2DeviceCoordinator.class),
|
||||||
TEST(1000, TestDeviceCoordinator.class);
|
TEST(1000, TestDeviceCoordinator.class);
|
||||||
|
|
||||||
private final int key;
|
private final int key;
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||||
|
|
||||||
|
public interface TemperatureSample extends TimeSample {
|
||||||
|
/**
|
||||||
|
* Returns the temperature value.
|
||||||
|
*/
|
||||||
|
float getTemperature();
|
||||||
|
/**
|
||||||
|
* Returns the temperature type (the position on the body where the measurement was taken).
|
||||||
|
*/
|
||||||
|
int getTemperatureType();
|
||||||
|
}
|
@ -310,7 +310,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
|||||||
if (gbDevice == null) {
|
if (gbDevice == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gbDevice.setFirmwareVersion(infoEvent.fwVersion);
|
if (infoEvent.fwVersion != null) {
|
||||||
|
gbDevice.setFirmwareVersion(infoEvent.fwVersion);
|
||||||
|
}
|
||||||
if (infoEvent.fwVersion2 != null) {
|
if (infoEvent.fwVersion2 != null) {
|
||||||
gbDevice.setFirmwareVersion2(infoEvent.fwVersion2);
|
gbDevice.setFirmwareVersion2(infoEvent.fwVersion2);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||||
|
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
|
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables or disables notifications for a given GATT characteristic.
|
* Enables or disables notifications for a given GATT characteristic.
|
||||||
* The result will be made available asynchronously through the
|
* The result will be made available asynchronously through the
|
||||||
@ -53,15 +52,16 @@ public class NotifyAction extends BtLEAction {
|
|||||||
if (notifyDescriptor != null) {
|
if (notifyDescriptor != null) {
|
||||||
int properties = getCharacteristic().getProperties();
|
int properties = getCharacteristic().getProperties();
|
||||||
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
|
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
|
||||||
LOG.debug("use NOTIFICATION");
|
LOG.debug("use NOTIFICATION for Characteristic " + getCharacteristic().getUuid());
|
||||||
notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
|
notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
|
||||||
result = gatt.writeDescriptor(notifyDescriptor);
|
result = gatt.writeDescriptor(notifyDescriptor);
|
||||||
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
|
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
|
||||||
LOG.debug("use INDICATION");
|
LOG.debug("use INDICATION for Characteristic " + getCharacteristic().getUuid());
|
||||||
notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
|
notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
|
||||||
result = gatt.writeDescriptor(notifyDescriptor);
|
result = gatt.writeDescriptor(notifyDescriptor);
|
||||||
hasWrittenDescriptor = true;
|
hasWrittenDescriptor = true;
|
||||||
} else {
|
} else {
|
||||||
|
LOG.debug("use neither NOTIFICATION nor INDICATION for Characteristic " + getCharacteristic().getUuid());
|
||||||
hasWrittenDescriptor = false;
|
hasWrittenDescriptor = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,6 +26,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
|||||||
public class ValueDecoder {
|
public class ValueDecoder {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class);
|
||||||
|
|
||||||
|
public static int decodeInt(BluetoothGattCharacteristic characteristic) {
|
||||||
|
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static int decodePercent(BluetoothGattCharacteristic characteristic) {
|
public static int decodePercent(BluetoothGattCharacteristic characteristic) {
|
||||||
int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
|
int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
|
||||||
if (percent > 100 || percent < 0) {
|
if (percent > 100 || percent < 0) {
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder;
|
||||||
|
|
||||||
|
/***
|
||||||
|
* This class handles the HealthThermometer as implemented on the Femometer Vinca II.
|
||||||
|
* This might or might not be up to GATT standard.
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
public class HealthThermometerProfile <T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(HealthThermometerProfile.class);
|
||||||
|
|
||||||
|
private static final String ACTION_PREFIX = HealthThermometerProfile.class.getName() + "_";
|
||||||
|
|
||||||
|
public static final String ACTION_TEMPERATURE_INFO = ACTION_PREFIX + "TEMPERATURE_INFO";
|
||||||
|
public static final String EXTRA_TEMPERATURE_INFO = "TEMPERATURE_INFO";
|
||||||
|
|
||||||
|
public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_HEALTH_THERMOMETER;
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT = GattCharacteristic.UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT;
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL = GattCharacteristic.UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL;
|
||||||
|
private final TemperatureInfo temperatureInfo = new TemperatureInfo();
|
||||||
|
|
||||||
|
public HealthThermometerProfile(T support) {
|
||||||
|
super(support);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestMeasurementInterval(TransactionBuilder builder) {
|
||||||
|
builder.read(getCharacteristic(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMeasurementInterval(TransactionBuilder builder, byte[] value) {
|
||||||
|
builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableNotify(TransactionBuilder builder, boolean enable) {
|
||||||
|
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT), enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
UUID charUuid = characteristic.getUuid();
|
||||||
|
if (charUuid.equals(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL)) {
|
||||||
|
handleMeasurementInterval(gatt, characteristic);
|
||||||
|
return true;
|
||||||
|
} else if (charUuid.equals(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT)) {
|
||||||
|
handleTemperatureMeasurement(gatt, characteristic);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
return onCharacteristicRead(gatt, characteristic, BluetoothGatt.GATT_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handleMeasurementInterval(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
// todo: not implemented
|
||||||
|
LOG.debug("Health thermometer received Measurement Interval: " + ValueDecoder.decodeInt(characteristic));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTemperatureMeasurement(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
/*
|
||||||
|
* This metadata contains as bits:
|
||||||
|
* the unit (celsius (0) or fahrenheit (1)) (bit 7 (last bit))
|
||||||
|
* if a timestamp is present (1) or not present (0) (bit 6)
|
||||||
|
* if a temperature type is present (1) or not present (0) (bit 5)
|
||||||
|
*/
|
||||||
|
byte metadata = characteristic.getValue()[0];
|
||||||
|
// todo: evaluate this byte to enable support for devices without timestamp or temperature-type
|
||||||
|
|
||||||
|
int year = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 5);
|
||||||
|
int month = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 7);
|
||||||
|
int day = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 8);
|
||||||
|
int hour = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 9);
|
||||||
|
int minute = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 10);
|
||||||
|
int second = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 11);
|
||||||
|
|
||||||
|
Calendar c = GregorianCalendar.getInstance();
|
||||||
|
c.set(year, month - 1, day, hour, minute, second);
|
||||||
|
Date date = c.getTime();
|
||||||
|
|
||||||
|
float temperature = characteristic.getFloatValue(BluetoothGattCharacteristic.FORMAT_FLOAT, 1); // bytes 1 - 4
|
||||||
|
int temperature_type = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 12); // encodes where the measurement was taken
|
||||||
|
|
||||||
|
LOG.debug("Received measurement of " + temperature + "° with Timestamp " + date + ", metadata is " + Integer.toBinaryString((metadata & 0xFF) + 0x100).substring(1));
|
||||||
|
|
||||||
|
temperatureInfo.setTemperature(temperature);
|
||||||
|
temperatureInfo.setTemperatureType(temperature_type);
|
||||||
|
temperatureInfo.setTimestamp(date);
|
||||||
|
notify(createIntent(temperatureInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent createIntent(TemperatureInfo temperatureInfo) {
|
||||||
|
Intent intent = new Intent(ACTION_TEMPERATURE_INFO);
|
||||||
|
intent.putExtra(EXTRA_TEMPERATURE_INFO, temperatureInfo);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class TemperatureInfo implements Parcelable{
|
||||||
|
|
||||||
|
private float temperature;
|
||||||
|
private int temperatureType;
|
||||||
|
private Date timestamp;
|
||||||
|
|
||||||
|
public TemperatureInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TemperatureInfo(Parcel in) {
|
||||||
|
timestamp = new Date(in.readLong());
|
||||||
|
temperature = in.readFloat();
|
||||||
|
temperatureType = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeLong(timestamp.getTime());
|
||||||
|
dest.writeFloat(temperature);
|
||||||
|
dest.writeInt(temperatureType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<TemperatureInfo> CREATOR = new Creator<TemperatureInfo>() {
|
||||||
|
@Override
|
||||||
|
public TemperatureInfo createFromParcel(Parcel in) {
|
||||||
|
return new TemperatureInfo(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TemperatureInfo[] newArray(int size) {
|
||||||
|
return new TemperatureInfo[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public float getTemperature() {
|
||||||
|
return temperature;
|
||||||
|
}
|
||||||
|
public Date getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
public void setTimestamp(Date date) {
|
||||||
|
timestamp = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemperature(float temperature) {
|
||||||
|
this.temperature = temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTemperatureType() {
|
||||||
|
return temperatureType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemperatureType(int temperatureType) {
|
||||||
|
this.temperatureType = temperatureType;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
/* Copyright (C) 2023 Alicia Hormann
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.femometer;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2SampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer.HealthThermometerProfile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer.TemperatureInfo;
|
||||||
|
|
||||||
|
public class FemometerVinca2DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||||
|
|
||||||
|
private final DeviceInfoProfile<FemometerVinca2DeviceSupport> deviceInfoProfile;
|
||||||
|
private final BatteryInfoProfile<FemometerVinca2DeviceSupport> batteryInfoProfile;
|
||||||
|
private final HealthThermometerProfile<FemometerVinca2DeviceSupport> healthThermometerProfile;
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FemometerVinca2DeviceSupport.class);
|
||||||
|
|
||||||
|
public static final UUID UNKNOWN_SERVICE_UUID = UUID.fromString((String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fef5")));
|
||||||
|
// Characteristic 8082caa8-41a6-4021-91c6-56f9b954cc34 READ WRITE
|
||||||
|
// Characteristic 9d84b9a3-000c-49d8-9183-855b673fda31 READ WRITE
|
||||||
|
// Characteristic 457871e8-d516-4ca1-9116-57d0b17b9cb2 READ WRITE NO RESPONSE WRITE
|
||||||
|
// Characteristic 5f78df94-798c-46f5-990a-b3eb6a065c88 READ NOTIFY
|
||||||
|
|
||||||
|
public static final UUID CONFIGURATION_SERVICE_UUID = UUID.fromString("0f0e0d0c-0b0a-0908-0706-050403020100");
|
||||||
|
public static final UUID CONFIGURATION_SERVICE_ALARM_CHARACTERISTIC = UUID.fromString("1f1e1d1c-1b1a-1918-1716-151413121110"); // READ WRITE
|
||||||
|
public static final UUID CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC = UUID.fromString("2f2e2d2c-2b2a-2928-2726-252423222120"); // WRITE
|
||||||
|
public static final UUID CONFIGURATION_SERVICE_INDICATION_CHARACTERISTIC = UUID.fromString("3f3e3d3c-3b3a-3938-3736-353433323130"); // INDICATE
|
||||||
|
|
||||||
|
public FemometerVinca2DeviceSupport() {
|
||||||
|
super(LOG);
|
||||||
|
|
||||||
|
/// Initialize Services
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_HEALTH_THERMOMETER);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_CURRENT_TIME);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_REFERENCE_TIME_UPDATE);
|
||||||
|
addSupportedService(UNKNOWN_SERVICE_UUID);
|
||||||
|
addSupportedService(CONFIGURATION_SERVICE_UUID);
|
||||||
|
|
||||||
|
/// Device Info
|
||||||
|
IntentListener deviceInfoListener = intent -> {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
|
||||||
|
DeviceInfo info = intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO);
|
||||||
|
if (info == null) return;
|
||||||
|
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||||
|
versionCmd.hwVersion = info.getHardwareRevision();
|
||||||
|
versionCmd.fwVersion = info.getSoftwareRevision(); // firmwareRevision always reported as null
|
||||||
|
handleGBDeviceEvent(versionCmd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||||
|
deviceInfoProfile.addListener(deviceInfoListener);
|
||||||
|
addSupportedProfile(deviceInfoProfile);
|
||||||
|
|
||||||
|
/// Battery
|
||||||
|
IntentListener batteryListener = intent -> {
|
||||||
|
BatteryInfo info = intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO);
|
||||||
|
if (info == null) return;
|
||||||
|
GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo();
|
||||||
|
batteryEvent.state = BatteryState.BATTERY_NORMAL;
|
||||||
|
batteryEvent.level = info.getPercentCharged();
|
||||||
|
evaluateGBDeviceEvent(batteryEvent);
|
||||||
|
handleGBDeviceEvent(batteryEvent);
|
||||||
|
};
|
||||||
|
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||||
|
batteryInfoProfile.addListener(batteryListener);
|
||||||
|
addSupportedProfile(batteryInfoProfile);
|
||||||
|
|
||||||
|
|
||||||
|
/// Temperature
|
||||||
|
IntentListener temperatureListener = intent -> {
|
||||||
|
TemperatureInfo info = intent.getParcelableExtra(HealthThermometerProfile.EXTRA_TEMPERATURE_INFO);
|
||||||
|
if (info == null) return;
|
||||||
|
handleMeasurement(info);
|
||||||
|
};
|
||||||
|
healthThermometerProfile = new HealthThermometerProfile<>(this);
|
||||||
|
healthThermometerProfile.addListener(temperatureListener);
|
||||||
|
addSupportedProfile(healthThermometerProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean useAutoConnect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data An int smaller equal 255 (0xff)
|
||||||
|
*/
|
||||||
|
private byte[] byteArray(int data) {
|
||||||
|
return new byte[]{(byte) data};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||||
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||||
|
|
||||||
|
// Init Battery
|
||||||
|
batteryInfoProfile.requestBatteryInfo(builder);
|
||||||
|
batteryInfoProfile.enableNotify(builder, true);
|
||||||
|
|
||||||
|
// Init Device Info
|
||||||
|
getDevice().setFirmwareVersion("N/A");
|
||||||
|
getDevice().setFirmwareVersion2("N/A");
|
||||||
|
deviceInfoProfile.requestDeviceInfo(builder);
|
||||||
|
|
||||||
|
// Mystery stuff that happens in original app, not sure if its required
|
||||||
|
BluetoothGattCharacteristic c2 = getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC);
|
||||||
|
builder.write(c2, byteArray(0x21));
|
||||||
|
builder.write(c2, byteArray(0x02));
|
||||||
|
builder.write(c2, byteArray(0x03));
|
||||||
|
builder.write(c2, byteArray(0x05));
|
||||||
|
|
||||||
|
// Sync Time
|
||||||
|
setCurrentTime(builder);
|
||||||
|
|
||||||
|
// Init Thermometer
|
||||||
|
builder.notify(getCharacteristic(CONFIGURATION_SERVICE_INDICATION_CHARACTERISTIC), true);
|
||||||
|
healthThermometerProfile.enableNotify(builder, true);
|
||||||
|
healthThermometerProfile.setMeasurementInterval(builder, new byte[]{(byte) 0x01, (byte) 0x00});
|
||||||
|
|
||||||
|
// mark the device as initialized
|
||||||
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetTime() {
|
||||||
|
TransactionBuilder builder = new TransactionBuilder("set time");
|
||||||
|
setCurrentTime(builder);
|
||||||
|
builder.queue(getQueue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentTime(TransactionBuilder builder) {
|
||||||
|
// Same Code as in PineTime (without the local time)
|
||||||
|
GregorianCalendar now = BLETypeConversions.createCalendar();
|
||||||
|
byte[] bytesCurrentTime = BLETypeConversions.calendarToCurrentTime(now, 0);
|
||||||
|
builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytesCurrentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||||
|
try {
|
||||||
|
TransactionBuilder builder = performInitialized("applyThermometerSetting");
|
||||||
|
|
||||||
|
Alarm alarm = alarms.get(0);
|
||||||
|
byte[] alarm_bytes = new byte[] {
|
||||||
|
(byte) (alarm.getEnabled()? 0x01 : 0x00), // first byte 01/00: turn alarm on/off
|
||||||
|
(byte) alarm.getHour(), // second byte: hour
|
||||||
|
(byte) alarm.getMinute() // third byte: minute
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.write(getCharacteristic(CONFIGURATION_SERVICE_ALARM_CHARACTERISTIC), alarm_bytes);
|
||||||
|
builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), byteArray(0x01));
|
||||||
|
// read-request on char1 results in given alarm
|
||||||
|
builder.queue(getQueue());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn(" Unable to apply setting ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendConfiguration(String config) {
|
||||||
|
TransactionBuilder builder;
|
||||||
|
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
|
||||||
|
LOG.info(" onSendConfiguration: " + config);
|
||||||
|
try {
|
||||||
|
builder = performInitialized("sendConfig: " + config);
|
||||||
|
switch (config) {
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_FEMOMETER_MEASUREMENT_MODE:
|
||||||
|
setMeasurementMode(sharedPreferences);
|
||||||
|
break;
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_VOLUME:
|
||||||
|
setVolume(sharedPreferences);
|
||||||
|
break;
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_TEMPERATURE_SCALE_CF:
|
||||||
|
String scale = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_TEMPERATURE_SCALE_CF, "c");
|
||||||
|
int value = "c".equals(scale) ? 0x0a : 0x0b;
|
||||||
|
applySetting(byteArray(value), null);
|
||||||
|
}
|
||||||
|
builder.queue(getQueue());
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set Measurement Mode
|
||||||
|
* modes (0- quick, 1- normal, 2- long)
|
||||||
|
*/
|
||||||
|
private void setMeasurementMode(SharedPreferences sharedPreferences) {
|
||||||
|
String measurementMode = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_FEMOMETER_MEASUREMENT_MODE, "normal");
|
||||||
|
byte[] confirmation = byteArray(0x1e);
|
||||||
|
switch (measurementMode) {
|
||||||
|
case "quick":
|
||||||
|
applySetting(byteArray(0x1a), confirmation);
|
||||||
|
break;
|
||||||
|
case "normal":
|
||||||
|
applySetting(byteArray(0x1c), confirmation);
|
||||||
|
break;
|
||||||
|
case "precise":
|
||||||
|
applySetting(byteArray(0x1d), confirmation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set Volume
|
||||||
|
* volumes 0-30 (0-10: quiet, 11-20: normal, 21-30: loud)
|
||||||
|
*/
|
||||||
|
private void setVolume(SharedPreferences sharedPreferences) {
|
||||||
|
int volume = sharedPreferences.getInt(DeviceSettingsPreferenceConst.PREF_VOLUME, 50);
|
||||||
|
byte[] confirmation = byteArray(0xfd);
|
||||||
|
if (volume < 11) {
|
||||||
|
applySetting(byteArray(0x09), confirmation);
|
||||||
|
} else if (volume < 21) {
|
||||||
|
applySetting(byteArray(0x14), confirmation);
|
||||||
|
} else {
|
||||||
|
applySetting(byteArray(0x16), confirmation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySetting(byte[] value, byte[] confirmation) {
|
||||||
|
try {
|
||||||
|
TransactionBuilder builder = performInitialized("applyThermometerSetting");
|
||||||
|
builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), value);
|
||||||
|
if (confirmation != null) {
|
||||||
|
builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), confirmation);
|
||||||
|
}
|
||||||
|
builder.queue(getQueue());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn(" Unable to apply setting ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMeasurement(TemperatureInfo info) {
|
||||||
|
Date timestamp = info.getTimestamp();
|
||||||
|
float temperature = info.getTemperature();
|
||||||
|
int temperatureType = info.getTemperatureType();
|
||||||
|
try (DBHandler db = GBApplication.acquireDB()) {
|
||||||
|
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
|
||||||
|
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
|
||||||
|
long time = timestamp.getTime();
|
||||||
|
|
||||||
|
FemometerVinca2SampleProvider sampleProvider = new FemometerVinca2SampleProvider(getDevice(), db.getDaoSession());
|
||||||
|
FemometerVinca2TemperatureSample temperatureSample = new FemometerVinca2TemperatureSample(time, deviceId, userId, temperature, temperatureType);
|
||||||
|
sampleProvider.addSample(temperatureSample);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error acquiring database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
app/src/main/res/drawable/ic_device_thermometer.xml
Normal file
6
app/src/main/res/drawable/ic_device_thermometer.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<vector android:height="45sp" android:viewportHeight="30" android:viewportWidth="30" android:width="45sp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#1f7fdb" android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115"/>
|
||||||
|
<path android:fillColor="#4dabf5" android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.879a0.947 0.947 0 0 1-0.947-0.948V3.982a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115"/>
|
||||||
|
<path android:fillColor="#2196f3" android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115"/>
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M14.37 22.271c-1.093 0-2.024-0.385-2.794-1.156-0.771-0.77-1.156-1.702-1.156-2.795 0-0.632 0.138-1.221 0.415-1.767 0.276-0.547 0.665-1.011 1.165-1.393V8.839c0-0.658 0.231-1.218 0.691-1.679 0.462-0.461 1.021-0.691 1.679-0.691 0.659 0 1.218 0.23 1.68 0.691 0.46 0.461 0.691 1.021 0.691 1.679v6.321c0.5 0.382 0.889 0.846 1.165 1.393 0.277 0.546 0.415 1.135 0.415 1.767 0 1.093-0.385 2.025-1.156 2.795-0.77 0.771-1.702 1.156-2.795 1.156zm-0.79-8.691h1.581v-0.79H14.37V12h0.791v-1.581H14.37v-0.79h0.791v-0.79c0-0.224-0.076-0.411-0.228-0.563-0.151-0.151-0.339-0.227-0.563-0.227-0.223 0-0.411 0.076-0.563 0.227-0.151 0.152-0.227 0.339-0.227 0.563v4.741z" android:strokeWidth="0.55"/>
|
||||||
|
</vector>
|
@ -0,0 +1,6 @@
|
|||||||
|
<vector android:height="45sp" android:viewportHeight="30" android:viewportWidth="30" android:width="45sp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#7a7a7a" android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115173"/>
|
||||||
|
<path android:fillAlpha="0.9411765" android:fillColor="#9f9f9f" android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.879a0.947 0.947 0 0 1-0.947-0.948V3.982a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115173"/>
|
||||||
|
<path android:fillColor="#8a8a8a" android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36a0.947 0.947 0 0 1 0.947-0.947z" android:strokeWidth="3.57115173"/>
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M14.37 22.271c-1.093 0-2.024-0.385-2.794-1.156-0.771-0.77-1.156-1.702-1.156-2.795 0-0.632 0.138-1.221 0.415-1.767 0.276-0.547 0.665-1.011 1.165-1.393V8.839c0-0.658 0.231-1.218 0.691-1.679 0.462-0.461 1.021-0.691 1.679-0.691 0.659 0 1.218 0.23 1.68 0.691 0.46 0.461 0.691 1.021 0.691 1.679v6.321c0.5 0.382 0.889 0.846 1.165 1.393 0.277 0.546 0.415 1.135 0.415 1.767 0 1.093-0.385 2.025-1.156 2.795-0.77 0.771-1.702 1.156-2.795 1.156zm-0.79-8.691h1.581v-0.79H14.37V12h0.791v-1.581H14.37v-0.79h0.791v-0.79c0-0.224-0.076-0.411-0.228-0.563-0.151-0.151-0.339-0.227-0.563-0.227-0.223 0-0.411 0.076-0.563 0.227-0.151 0.152-0.227 0.339-0.227 0.563v4.741z" android:strokeWidth="0.55"/>
|
||||||
|
</vector>
|
@ -3411,4 +3411,16 @@
|
|||||||
<item>c</item>
|
<item>c</item>
|
||||||
<item>f</item>
|
<item>f</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="femometer_measurement_mode">
|
||||||
|
<item>@string/femometer_measurement_mode_quick</item>
|
||||||
|
<item>@string/femometer_measurement_mode_normal</item>
|
||||||
|
<item>@string/femometer_measurement_mode_precise</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="femometer_measurement_mode_values">
|
||||||
|
<item>quick</item>
|
||||||
|
<item>normal</item>
|
||||||
|
<item>precise</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -428,6 +428,11 @@
|
|||||||
<string name="power_mode_normal">Normal</string>
|
<string name="power_mode_normal">Normal</string>
|
||||||
<string name="power_mode_saving">Power saving</string>
|
<string name="power_mode_saving">Power saving</string>
|
||||||
<string name="power_mode_watch">Only watch</string>
|
<string name="power_mode_watch">Only watch</string>
|
||||||
|
<!-- Femometer Preferences -->
|
||||||
|
<string name="femometer_measurement_mode_title">Measurement mode</string>
|
||||||
|
<string name="femometer_measurement_mode_quick">Quick Mode (30s)</string>
|
||||||
|
<string name="femometer_measurement_mode_normal">Normal Mode (60s-90s)</string>
|
||||||
|
<string name="femometer_measurement_mode_precise"> Precise Mode (3min) </string>
|
||||||
<!-- Makibes HR3 Preferences -->
|
<!-- Makibes HR3 Preferences -->
|
||||||
<string name="preferences_makibes_hr3_settings">Makibes HR3 settings</string>
|
<string name="preferences_makibes_hr3_settings">Makibes HR3 settings</string>
|
||||||
<!-- ID115 Preferences -->
|
<!-- ID115 Preferences -->
|
||||||
@ -1373,6 +1378,7 @@
|
|||||||
<string name="devicetype_sony_wf_1000xm5">Sony WF-1000XM5</string>
|
<string name="devicetype_sony_wf_1000xm5">Sony WF-1000XM5</string>
|
||||||
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
|
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
|
||||||
<string name="devicetype_binary_sensor">Binary sensor</string>
|
<string name="devicetype_binary_sensor">Binary sensor</string>
|
||||||
|
<string name="devicetype_femometer_vinca2">Femometer Vinca II</string>
|
||||||
<string name="choose_auto_export_location">Choose export location</string>
|
<string name="choose_auto_export_location">Choose export location</string>
|
||||||
<string name="notification_channel_name">General</string>
|
<string name="notification_channel_name">General</string>
|
||||||
<string name="notification_channel_high_priority_name">High-priority</string>
|
<string name="notification_channel_high_priority_name">High-priority</string>
|
||||||
|
11
app/src/main/res/xml/devicesettings_femometer.xml
Normal file
11
app/src/main/res/xml/devicesettings_femometer.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<ListPreference
|
||||||
|
android:entries="@array/femometer_measurement_mode"
|
||||||
|
android:entryValues="@array/femometer_measurement_mode_values"
|
||||||
|
android:defaultValue="normal"
|
||||||
|
android:icon="@drawable/ic_checklist"
|
||||||
|
android:key="femometer_measurement_mode"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="@string/femometer_measurement_mode_title" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user