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:
ahormann 2023-10-15 13:37:41 +00:00 committed by José Rebelo
parent 11de66f8e4
commit 28e673415f
21 changed files with 845 additions and 5 deletions

View File

@ -38,12 +38,14 @@ public class GBDaoGenerator {
private static final String SAMPLE_STEPS = "steps";
private static final String SAMPLE_RAW_KIND = "rawKind";
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_TO = "timestampTo";
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 user = addUserInfo(schema, userAttributes);
@ -101,6 +103,7 @@ public class GBDaoGenerator {
addWena3HeartRateSample(schema, user, device);
addWena3Vo2Sample(schema, user, device);
addWena3StressSample(schema, user, device);
addFemometerVinca2TemperatureSample(schema, user, device);
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
@ -938,4 +941,17 @@ public class GBDaoGenerator {
perAppSetting.addStringProperty("vibrationRepetition");
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;
}
}

View File

@ -362,4 +362,5 @@ public class DeviceSettingsPreferenceConst {
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_FEMOMETER_MEASUREMENT_MODE = "femometer_measurement_mode";
}

View File

@ -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_TIMEOUT);
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);

View File

@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -190,6 +191,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return null;
}
@Override
public TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
return null;

View File

@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
@ -235,6 +236,11 @@ public interface DeviceCoordinator {
*/
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.
*/

View File

@ -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,
};
}
}

View File

@ -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();
}
}

View File

@ -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() +
"}";
}
}

View File

@ -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.CasioGWB5600DeviceCoordinator;
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.flipper.zero.FlipperZeroCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
@ -265,6 +266,8 @@ public enum DeviceType {
SOFLOW_SO6(550, SoFlowCoordinator.class),
WITHINGS_STEEL_HR(560, WithingsSteelHRDeviceCoordinator.class),
SONY_WENA_3(570, SonyWena3Coordinator.class),
FEMOMETER_VINCA2(580, FemometerVinca2DeviceCoordinator.class),
TEST(1000, TestDeviceCoordinator.class);
private final int key;

View File

@ -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();
}

View File

@ -310,7 +310,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
if (gbDevice == null) {
return;
}
gbDevice.setFirmwareVersion(infoEvent.fwVersion);
if (infoEvent.fwVersion != null) {
gbDevice.setFirmwareVersion(infoEvent.fwVersion);
}
if (infoEvent.fwVersion2 != null) {
gbDevice.setFirmwareVersion2(infoEvent.fwVersion2);
}

View File

@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
/**
* Enables or disables notifications for a given GATT characteristic.
* The result will be made available asynchronously through the
@ -53,15 +52,16 @@ public class NotifyAction extends BtLEAction {
if (notifyDescriptor != null) {
int properties = getCharacteristic().getProperties();
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);
result = gatt.writeDescriptor(notifyDescriptor);
} 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);
result = gatt.writeDescriptor(notifyDescriptor);
hasWrittenDescriptor = true;
} else {
LOG.debug("use neither NOTIFICATION nor INDICATION for Characteristic " + getCharacteristic().getUuid());
hasWrittenDescriptor = false;
}
} else {

View File

@ -26,6 +26,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
public class ValueDecoder {
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) {
int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
if (percent > 100 || percent < 0) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View 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>

View 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="#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>

View File

@ -3411,4 +3411,16 @@
<item>c</item>
<item>f</item>
</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>

View File

@ -428,6 +428,11 @@
<string name="power_mode_normal">Normal</string>
<string name="power_mode_saving">Power saving</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 -->
<string name="preferences_makibes_hr3_settings">Makibes HR3 settings</string>
<!-- ID115 Preferences -->
@ -1373,6 +1378,7 @@
<string name="devicetype_sony_wf_1000xm5">Sony WF-1000XM5</string>
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</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="notification_channel_name">General</string>
<string name="notification_channel_high_priority_name">High-priority</string>

View 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>