mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
OVERMAX Touch 2.6: Initial support
This commit is contained in:
@@ -75,6 +75,7 @@ public class GBDaoGenerator {
|
||||
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
|
||||
|
||||
addMakibesHR3ActivitySample(schema, user, device);
|
||||
addOVTouch26ActivitySample(schema, user, device);
|
||||
addMiBandActivitySample(schema, user, device);
|
||||
addHuamiExtendedActivitySample(schema, user, device);
|
||||
addHuamiStressSample(schema, user, device);
|
||||
@@ -340,6 +341,19 @@ public class GBDaoGenerator {
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addOVTouch26ActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "OVTouch26ActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_SYSTOLIC).notNull();
|
||||
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_DIASTOLIC).notNull();
|
||||
activitySample.addIntProperty("sleep");
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "MiBandActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/* Copyright (C) 2019-2025 vappster
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.overmax;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class OVTouch26Constants {
|
||||
|
||||
public static final UUID UUID_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_REPORT = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
|
||||
//Packet structure (commands)
|
||||
//ab 00 (payload length) (payload crc) (packet seq#) (unknown) 00 (cmd) 00 [args]
|
||||
// ^--------- payload --------^
|
||||
//payload len, payload crc and packet seq# are uint16, unknown and cmd are uint8
|
||||
//checksum is crc16/arc and is calculated starting from the (unknown) variable until the end
|
||||
//of the packet
|
||||
//packet seq# is a counter that increments itself for every packet sent to the device
|
||||
//NOTE:
|
||||
//Based on the known possible values, the unknown variable here seems to at least control which
|
||||
//ack id to use for the command's associated ACK request (see below). However, there may be more
|
||||
//to this variable than just that (for example, why do two completely unrelated commands like
|
||||
//CMD_FIND_DEVICE and CMD_NOTIFICATION use the same value?) so I'm not *fully* sure of this yet
|
||||
|
||||
public static final byte[] PACKET_CMD_TEMPLATE = {
|
||||
(byte) 0xab,
|
||||
(byte) 0x00,
|
||||
(byte) 0, //length
|
||||
(byte) 0,
|
||||
(byte) 0, //crc16 arc
|
||||
(byte) 0,
|
||||
(byte) 0, //packet seq
|
||||
(byte) 0x01,
|
||||
(byte) 0, //unknown (see note above)
|
||||
(byte) 0x00,
|
||||
(byte) 0, //cmd
|
||||
(byte) 0x00
|
||||
// ,arguments
|
||||
};
|
||||
|
||||
//Packet structure (ACK reply)
|
||||
//ab 10 00 00 00 00 (msg id)
|
||||
//
|
||||
//msg id is uint16 and is given to the client via an ACK request with the following structure:
|
||||
//ab 00 00 (ack id) (unknown) (msg id)
|
||||
//ack id is uint8 and it seems to be a value used to differentiate which cmd requested the ack
|
||||
//unknown is uint16 (doesn't seem to be crc)
|
||||
public static final byte[] PACKET_ACK_TEMPLATE = {
|
||||
(byte) 0xab,
|
||||
(byte) 0x10,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0, //msg id
|
||||
(byte) 0
|
||||
};
|
||||
|
||||
public static final byte[] ARGS_NONE = {
|
||||
(byte) 0x00
|
||||
};
|
||||
|
||||
public static final byte[] ARGS_FIND_DEVICE = {
|
||||
(byte) 0x01,
|
||||
(byte) 0x01
|
||||
};
|
||||
|
||||
public static final byte[] CMD_FIND_DEVICE = new byte[]{(byte) 0x04, 0x61};
|
||||
public static final byte[] CMD_GET_STEPS = new byte[]{(byte) 0x05, 0x41};
|
||||
public static final byte[] CMD_GET_HEART_RATE = new byte[]{(byte) 0x05, 0x43};
|
||||
public static final byte[] CMD_BATTERY_INFO = new byte[]{(byte) 0x02, 0x08};
|
||||
public static final byte[] CMD_NOTIFICATION = new byte[]{(byte) 0x04, 0x52};
|
||||
|
||||
//Known reply values
|
||||
public static final int REPLY_HEADER_BATTERY = 0x02000900;
|
||||
public static final int REPLY_HEADER_STEPS = 0x05004200;
|
||||
public static final int REPLY_HEADER_HEART_RATE = 0x05004400;
|
||||
public static final int REPLY_HEADER_FIND_MY_PHONE = 0x04006000;
|
||||
public static final int REPLY_HEADER_ERROR = 0xe6000000;
|
||||
public static final int REPLY_HEADER_ACK = 0xab0000;
|
||||
|
||||
/* A note about pairing and bonding
|
||||
|
||||
When pairing the watch in its proprietary app, the following command & args sent to the watch:
|
||||
------------------------------------------------------------------------------------------------
|
||||
public static final byte[] ARGS_BIND = {
|
||||
(byte) 0x20, //arg length, 1-based (excludes itself from length), always 0x20
|
||||
(byte) 0xff //always 0xff
|
||||
//The next 31 bytes appear to all always be 0x00
|
||||
};
|
||||
|
||||
public static final byte[] CMD_BIND = new byte[]{(byte) 0x03, 0x01};
|
||||
------------------------------------------------------------------------------------------------
|
||||
The command will trigger a screen on the device in which the user can choose whether to pair the
|
||||
device to the phone. Once the user makes their decision, their choice will get reported to the
|
||||
app, which will then proceed accordingly.
|
||||
|
||||
However, on the watch's side, the command's only purpose is exactly that: display the screen and
|
||||
return the resulting boolean choice. No other info about either device is sent at any point nor
|
||||
any further action is taken on the watch's side. The watch will otherwise allow anyone to
|
||||
connect, bond and send/receive commands regardless of whether this screen has been shown before
|
||||
(in fact, all commands work even if the device is connected but not bound!). The pairing and
|
||||
bonding process is handled entirely by the app via standard BTLE procedures.
|
||||
|
||||
Therefore, the command is not actually needed for the binding process and it only is an extra
|
||||
arbitrary step intended specifically for the proprietary app. Due to this, it can be skipped
|
||||
entirely and, as Gadgetbridge can effectively give the user additional choices compared to the
|
||||
proprietary app via its BONDING_STYLE_ASK setting (for example whether to bond and to add the
|
||||
device as a companion) this command is unused in the current implementation. */
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/* Copyright (C) 2019-2025 vappster
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.overmax;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BangleJSActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CasioGBX100ActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.OVTouch26ActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.overmax.OVTouch26DeviceSupport;
|
||||
|
||||
public class OVTouch26Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_ovtouch_26;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_miwatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Overmax";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
|
||||
return OVTouch26DeviceSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("OV-Touch2.6_LE");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_timeformat,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_disconnectnotification,
|
||||
R.xml.devicesettings_donotdisturb_no_auto,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_transliteration
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceCoordinator.DeviceKind getDeviceKind(@NonNull GBDevice device) {
|
||||
return DeviceCoordinator.DeviceKind.FITNESS_BAND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new OVTouch26SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<AbstractDao<?, ?>, Property> getAllDeviceDao(@NonNull final DaoSession session) {
|
||||
Map<AbstractDao<?, ?>, Property> map = new HashMap<>(1);
|
||||
map.put(session.getOVTouch26ActivitySampleDao(), OVTouch26ActivitySampleDao.Properties.DeviceId);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* Copyright (C) 2019-2025 vappster, José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.overmax;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.OVTouch26ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.OVTouch26ActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class OVTouch26SampleProvider extends AbstractSampleProvider<OVTouch26ActivitySample> {
|
||||
public static final int TYPE_ACTIVITY = 1;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OVTouch26SampleProvider.class);
|
||||
|
||||
public OVTouch26SampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivityKind normalizeType(int rawType) {
|
||||
return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(ActivityKind activityKind) {
|
||||
return activityKind.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity / 255.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OVTouch26ActivitySample createActivitySample() {
|
||||
return new OVTouch26ActivitySample();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<OVTouch26ActivitySample, ?> getSampleDao() {
|
||||
return getSession().getOVTouch26ActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return OVTouch26ActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return OVTouch26ActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return OVTouch26ActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<OVTouch26ActivitySample> getGBActivitySamples(final int timestamp_from, final int timestamp_to) {
|
||||
//Code from GarminActivitySampleProvider, thank you José Rebelo!
|
||||
LOG.trace(
|
||||
"Getting OVTouch26 activity samples between {} and {}",
|
||||
timestamp_from,
|
||||
timestamp_to
|
||||
);
|
||||
|
||||
final long nanoStart = System.nanoTime();
|
||||
|
||||
final List<OVTouch26ActivitySample> samples = fillGaps(
|
||||
super.getGBActivitySamples(timestamp_from + 60, timestamp_to + 60),
|
||||
timestamp_from + 60,
|
||||
timestamp_to + 60
|
||||
);
|
||||
|
||||
samples.forEach(s -> s.setTimestamp(s.getTimestamp() - 60));
|
||||
|
||||
if (!samples.isEmpty()) {
|
||||
convertCumulativeSteps(samples, OVTouch26ActivitySampleDao.Properties.Steps);
|
||||
}
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
|
||||
LOG.trace("Getting OVTouch26 samples took {}ms", executionTime);
|
||||
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
@@ -334,6 +334,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.onemoresonoflow.OneMoreSonoF
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAir2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoBuds2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.overmax.OVTouch26Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pixel.PixelBudsACoordinator;
|
||||
@@ -569,6 +570,7 @@ public enum DeviceType {
|
||||
MICOMPOSITIONSCALE(MiCompositionScaleCoordinator.class),
|
||||
BFH16(BFH16DeviceCoordinator.class),
|
||||
MAKIBESHR3(MakibesHR3Coordinator.class),
|
||||
OVTOUCH26(OVTouch26Coordinator.class),
|
||||
BANGLEJS(BangleJSCoordinator.class),
|
||||
FOSSILQHYBRID(QHybridCoordinator.class),
|
||||
TLW64(TLW64Coordinator.class),
|
||||
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
/* Copyright (C) 2019-2025 Andreas Shimokawa, Arjan Schrijver, Cre3per,
|
||||
Damien Gaignon, Taavi Eomäe, vappster
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
//TODO: Implement SpO2, sleep and sports data sync, as well as various misc commands (for ex.
|
||||
//smart camera, automatic date & time sync, etc.)
|
||||
//TODO: Various settings (device wake gesture, alarms, etc.)
|
||||
//TODO: Firmware updates
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.overmax;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
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.GBDeviceEventFindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.overmax.OVTouch26Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.overmax.OVTouch26SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.OVTouch26ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class OVTouch26DeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OVTouch26DeviceSupport.class);
|
||||
private final DeviceInfoProfile<OVTouch26DeviceSupport> deviceInfoProfile;
|
||||
int packet_seq = 0x0000;
|
||||
private BluetoothGattCharacteristic mControlCharacteristic = null;
|
||||
private BluetoothGattCharacteristic mReportCharacteristic = null;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable batteryRunner = () -> {
|
||||
LOG.info("Running retrieving battery through runner.");
|
||||
sendCmd(OVTouch26Constants.CMD_BATTERY_INFO, OVTouch26Constants.ARGS_NONE, "getBatteryInfo");
|
||||
};
|
||||
private OVTouch26ActivitySample CurrentSample = null;
|
||||
|
||||
public OVTouch26DeviceSupport() {
|
||||
super(LOG);
|
||||
|
||||
addSupportedService(OVTouch26Constants.UUID_SERVICE);
|
||||
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
|
||||
|
||||
IntentListener mListener = new IntentListener() {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
|
||||
handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||
deviceInfoProfile.addListener(mListener);
|
||||
addSupportedProfile(deviceInfoProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] craftData(byte[] command, byte[] data) {
|
||||
byte[] payload = new byte[data.length + 4];
|
||||
byte[] result = new byte[OVTouch26Constants.PACKET_CMD_TEMPLATE.length + data.length];
|
||||
int crc;
|
||||
|
||||
System.arraycopy(OVTouch26Constants.PACKET_CMD_TEMPLATE, 0, result, 0, OVTouch26Constants.PACKET_CMD_TEMPLATE.length);
|
||||
|
||||
//The unknown and command bytes are part of the checksum-protected data
|
||||
result[8] = command[0];
|
||||
result[10] = command[1];
|
||||
|
||||
System.arraycopy(data, 0, result, OVTouch26Constants.PACKET_CMD_TEMPLATE.length, data.length);
|
||||
System.arraycopy(result, 8, payload, 0, payload.length);
|
||||
crc = CheckSums.getCRC16ansi(payload, 0x0000);
|
||||
|
||||
//All of these are not checksum-protected
|
||||
result[2] = (byte) ((data.length + 4 >> 8) & 0xFF);
|
||||
result[3] = (byte) (data.length + 4 & 0xFF);
|
||||
result[4] = (byte) ((crc >> 8) & 0xFF);
|
||||
result[5] = (byte) (crc & 0xFF);
|
||||
result[6] = (byte) ((packet_seq >> 8) & 0xFF);
|
||||
result[7] = (byte) (packet_seq & 0xFF);
|
||||
|
||||
if (packet_seq == 0xFFFF) {
|
||||
packet_seq = 0;
|
||||
} else {
|
||||
packet_seq++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private OVTouch26ActivitySample createActivitySample() {
|
||||
OVTouch26ActivitySample sample = new OVTouch26ActivitySample();
|
||||
|
||||
sample.setTimestamp(-1);
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
sample.setRawKind(OVTouch26SampleProvider.TYPE_ACTIVITY);
|
||||
sample.setSteps(ActivitySample.NOT_MEASURED);
|
||||
sample.setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
private void sendTransaction(byte[] data, String transactionName) {
|
||||
TransactionBuilder transactionBuilder = this.createTransactionBuilder(transactionName);
|
||||
|
||||
transactionBuilder.write(this.mControlCharacteristic, data);
|
||||
try {
|
||||
transactionBuilder.queueConnected();
|
||||
} catch (Exception ex) {
|
||||
LOG.warn(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendCmd(byte[] cmd, byte[] args, String transactionName) {
|
||||
byte[] data = this.craftData(cmd, args);
|
||||
|
||||
sendTransaction(data, transactionName);
|
||||
}
|
||||
|
||||
private void sendAck(byte[] packet) {
|
||||
byte[] ackPacket = OVTouch26Constants.PACKET_ACK_TEMPLATE;
|
||||
ackPacket[6] = packet[packet.length - 2];
|
||||
ackPacket[7] = packet[packet.length - 1];
|
||||
|
||||
sendTransaction(ackPacket, "sendAckPacket");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
CurrentSample = createActivitySample();
|
||||
sendCmd(OVTouch26Constants.CMD_GET_STEPS, OVTouch26Constants.ARGS_NONE, "sendGetStepsCmd");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
if (!start) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendCmd(OVTouch26Constants.CMD_FIND_DEVICE, OVTouch26Constants.ARGS_FIND_DEVICE, "findDevice");
|
||||
}
|
||||
|
||||
public boolean startBatteryRunnerDelayed() {
|
||||
int interval_minutes = GBApplication.getDevicePrefs(gbDevice).getBatteryPollingIntervalMinutes();
|
||||
int interval = interval_minutes * 60 * 1000;
|
||||
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval, interval_minutes);
|
||||
handler.removeCallbacks(batteryRunner);
|
||||
return handler.postDelayed(batteryRunner, interval);
|
||||
}
|
||||
|
||||
public void stopBatteryRunnerDelayed() {
|
||||
LOG.debug("Stopping battery runner delayed");
|
||||
handler.removeCallbacks(batteryRunner);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
packet_seq = 0x0000;
|
||||
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 0, getContext());
|
||||
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZING);
|
||||
deviceInfoProfile.requestDeviceInfo(builder);
|
||||
|
||||
this.mControlCharacteristic = getCharacteristic(OVTouch26Constants.UUID_CHARACTERISTIC_CONTROL);
|
||||
this.mReportCharacteristic = getCharacteristic(OVTouch26Constants.UUID_CHARACTERISTIC_REPORT);
|
||||
|
||||
builder.notify(this.mReportCharacteristic, true);
|
||||
builder.setCallback(this);
|
||||
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZED);
|
||||
|
||||
//Send battery info command if polling is enabled to show the charge % in the devices screen as soon as we're connected
|
||||
//(It's too early to use the sendCmd func so we use the following instead)
|
||||
if (GBApplication.getDevicePrefs(getDevice()).getBatteryPollingEnabled()) {
|
||||
builder.write(this.mControlCharacteristic, this.craftData(OVTouch26Constants.CMD_BATTERY_INFO, OVTouch26Constants.ARGS_NONE));
|
||||
}
|
||||
|
||||
GB.updateTransferNotification(null, "", false, 100, getContext());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void handleDeviceInfo(DeviceInfo info) {
|
||||
LOG.debug("Device info: {}", info);
|
||||
|
||||
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
versionCmd.hwVersion = info.getHardwareRevision();
|
||||
versionCmd.fwVersion = info.getFirmwareRevision();
|
||||
handleGBDeviceEvent(versionCmd);
|
||||
}
|
||||
|
||||
private void parseAndSetSampleTimestamp(byte[] packet, int offset) {
|
||||
//The sample timestamps reported by this watch report how many seconds have passed since 01/01/2000 at 00:00.
|
||||
//Therefore an easy way to convert them in a timestamp understandable by Gadgetbridge is to convert them to
|
||||
//Unix time by adding 946681200 seconds (= the difference to Unix epoch)
|
||||
if (CurrentSample.getTimestamp() == -1) { //If the timestamp has already been set by a previous cmd reply, don't overwrite it
|
||||
int timestamp = BLETypeConversions.toUint32(packet[packet.length - offset], packet[packet.length - (offset + 1)], packet[packet.length - (offset + 2)], packet[packet.length - (offset + 3)]) + 946681200;
|
||||
CurrentSample.setTimestamp(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCurrentSample() {
|
||||
if (CurrentSample.getTimestamp() != -1) { //If timestamp is -1 it means no data has been recorded and the current sample can be discarded
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
DaoSession session = handler.getDaoSession();
|
||||
|
||||
OVTouch26SampleProvider provider = new OVTouch26SampleProvider(gbDevice, session);
|
||||
Device device = DBHelper.getDevice(getDevice(), handler.getDaoSession());
|
||||
User user = DBHelper.getUser(handler.getDaoSession());
|
||||
CurrentSample.setDevice(device);
|
||||
CurrentSample.setUser(user);
|
||||
CurrentSample.setProvider(provider);
|
||||
provider.addGBActivitySample(CurrentSample);
|
||||
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
|
||||
.putExtra(GBDevice.EXTRA_DEVICE, getDevice())
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, CurrentSample)
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, CurrentSample.getTimestamp());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error acquiring database", e);
|
||||
}
|
||||
}
|
||||
CurrentSample = null;
|
||||
GB.updateTransferNotification(null, "", false, 100, getContext()); //Clear notification
|
||||
GB.signalActivityDataFinish(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
|
||||
//The sample requests in the proprietary app always happens in this sequence:
|
||||
//Steps -> Heart Rate -> (SpO2) -> (Sleep) -> (Sports data)
|
||||
//We try to stick to that order here
|
||||
switch (BLETypeConversions.toUint32(value[3], value[2], value[1], value[0])) { //Reply packet header, little endian
|
||||
case OVTouch26Constants.REPLY_HEADER_BATTERY:
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
|
||||
batteryInfo.level = value[5];
|
||||
batteryInfo.state = BatteryState.UNKNOWN;
|
||||
|
||||
this.handleGBDeviceEvent(batteryInfo);
|
||||
//Restart the battery runner once the packet has been handled
|
||||
//(no need to check if battery polling is enabled as we only ever request battery info if it's enabled to begin with)
|
||||
startBatteryRunnerDelayed();
|
||||
break;
|
||||
case OVTouch26Constants.REPLY_HEADER_STEPS:
|
||||
//Sometimes when the device errors out it can return a much longer packet with what looks to be garbage data. In that case, we can just ignore it
|
||||
//TODO: On the other hand, in *very* rare cases (for ex. multiple dropped packets in the span of 2+ days) the packet can include two samples with
|
||||
//multiple timestamps (so far it seems like two at a time is the limit), add logic to parse those as well
|
||||
if (CurrentSample != null && value.length == 13) {
|
||||
int steps = BLETypeConversions.toUint32(value[value.length - 1], value[value.length - 2], value[value.length - 3], value[value.length - 4]);
|
||||
parseAndSetSampleTimestamp(value, 5);
|
||||
CurrentSample.setSteps(steps);
|
||||
sendCmd(OVTouch26Constants.CMD_GET_HEART_RATE, OVTouch26Constants.ARGS_NONE, "getHeartRate");
|
||||
}
|
||||
break;
|
||||
case OVTouch26Constants.REPLY_HEADER_HEART_RATE:
|
||||
if (CurrentSample != null) {
|
||||
if (value.length >= 10){
|
||||
parseAndSetSampleTimestamp(value, 2);
|
||||
CurrentSample.setHeartRate(value[value.length - 1]);
|
||||
}
|
||||
addCurrentSample(); //Always try to add the sample, even if heart rate was not reported
|
||||
}
|
||||
break;
|
||||
case OVTouch26Constants.REPLY_HEADER_FIND_MY_PHONE:
|
||||
GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
if (value[5] == 0x01) {
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
|
||||
} else {
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
||||
}
|
||||
evaluateGBDeviceEvent(findPhoneEvent);
|
||||
break;
|
||||
case OVTouch26Constants.REPLY_HEADER_ERROR: //If the watch returns an error the sent data is likely garbage/incorrect
|
||||
LOG.debug("device reported error, invalidating sample!");
|
||||
CurrentSample = null;
|
||||
break;
|
||||
default:
|
||||
if (BLETypeConversions.toUint24(value[2], value[1], value[0]) == OVTouch26Constants.REPLY_HEADER_ACK) {
|
||||
//Technically speaking, most packets seem to have their own distinct ACK requests identifiable by an unique
|
||||
//ack id in the header (uint8, offset 0x3). Practically, they all follow the same request/response structure,
|
||||
//and failing to reply to enough of them *will* eventually make the watch return an error
|
||||
//Therefore, it's better to always reply to ACKs requests, regardless if the associated command is currently
|
||||
//implemented or otherwise
|
||||
sendAck(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
String body = notificationSpec.body;
|
||||
String title = notificationSpec.title;
|
||||
if (title.isEmpty()) {
|
||||
title = "Notification";
|
||||
}
|
||||
if (body.isEmpty()) {
|
||||
body = title;
|
||||
}
|
||||
int titleLength = title.length();
|
||||
int bodyLength = body.length();
|
||||
if (titleLength > 32) {
|
||||
titleLength = 32;
|
||||
}
|
||||
if (bodyLength > 223) {
|
||||
bodyLength = 223;
|
||||
} //Length is uint8 and its initial value is 32 (see below) so the body cannot exceed 255-32=223 bytes
|
||||
byte[] args = new byte[33 + bodyLength]; //1 byte for len + 32 bytes are *always* allocated for the title, even if it's shorter than that
|
||||
|
||||
Arrays.fill(args, (byte) 0x00);
|
||||
args[0] = (byte) (32 + bodyLength);
|
||||
System.arraycopy(title.getBytes(), 0, args, 1, titleLength);
|
||||
System.arraycopy(body.getBytes(), 0, args, 33, bodyLength);
|
||||
|
||||
sendCmd(OVTouch26Constants.CMD_NOTIFICATION, args, "onNotification");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
//NOTE: The proprietary app for this watch does not support on-demand heart rate reporting, meaning it will
|
||||
//simply display the value of the last heart rate sample taken by the watch either through periodic tests
|
||||
//or manually by pressing the associated icon on the watch's UI. As such, there seems to be no command for
|
||||
//an on-demand heart rate test, and because the watch clears that value from memory once it's sent, there
|
||||
//is a chance for this function to not return any value if, for example, it's used twice in a row
|
||||
CurrentSample = createActivitySample();
|
||||
sendCmd(OVTouch26Constants.CMD_GET_HEART_RATE, OVTouch26Constants.ARGS_NONE, "getHeartRate");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getImplicitCallbackModify() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getSendWriteRequestResponse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
|
||||
if (!GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) {
|
||||
stopBatteryRunnerDelayed();
|
||||
break;
|
||||
}
|
||||
// Fall through if enabled
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
|
||||
if (!startBatteryRunnerDelayed()) {
|
||||
GB.toast(getContext(), R.string.battery_polling_failed_start, Toast.LENGTH_SHORT, GB.ERROR);
|
||||
LOG.error("Failed to start the battery polling");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopBatteryRunnerDelayed();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Damien
|
||||
Gaignon, Daniele Gobbetti, José Rebelo, Petr Vaněk
|
||||
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, Damien
|
||||
Gaignon, Daniele Gobbetti, José Rebelo, Petr Vaněk, vappster
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -73,7 +73,10 @@ public class CheckSums {
|
||||
}
|
||||
|
||||
public static int getCRC16ansi(byte[] seq) {
|
||||
int crc = 0xffff;
|
||||
return getCRC16ansi(seq, 0xffff);
|
||||
}
|
||||
|
||||
public static int getCRC16ansi(byte[] seq, int crc) {
|
||||
int polynomial = 0xA001;
|
||||
|
||||
for (int i = 0; i < seq.length; i++) {
|
||||
|
||||
@@ -2025,6 +2025,7 @@
|
||||
<string name="devicetype_mijia_xmwsdj04" translatable="false">Mijia Temperature and Humidity Sensor 2 (E-ink)</string>
|
||||
<string name="devicetype_mijia_mho_c303" translatable="false">Mijia MHO-C303</string>
|
||||
<string name="devicetype_makibes_hr3" translatable="false">Makibes HR3</string>
|
||||
<string name="devicetype_ovtouch_26" translatable="false">Overmax Touch 2.6</string>
|
||||
<string name="devicetype_banglejs" translatable="false">Bangle.js</string>
|
||||
<string name="devicetype_tlw64" translatable="false">TLW64</string>
|
||||
<string name="devicetype_pinetime_jf" translatable="false">PineTime (JF Firmware)</string>
|
||||
|
||||
+1
@@ -159,6 +159,7 @@ public class AbstractDeviceCoordinatorTest extends TestBase {
|
||||
put("M4-4711", DeviceType.FITPRO);
|
||||
put("C20", DeviceType.C20); // #4070
|
||||
put("C 20", DeviceType.C20); // #5495
|
||||
put("OV-Touch2.6_LE", DeviceType.OVTOUCH26);
|
||||
}};
|
||||
|
||||
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
||||
|
||||
Reference in New Issue
Block a user