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);
|
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
|
||||||
|
|
||||||
addMakibesHR3ActivitySample(schema, user, device);
|
addMakibesHR3ActivitySample(schema, user, device);
|
||||||
|
addOVTouch26ActivitySample(schema, user, device);
|
||||||
addMiBandActivitySample(schema, user, device);
|
addMiBandActivitySample(schema, user, device);
|
||||||
addHuamiExtendedActivitySample(schema, user, device);
|
addHuamiExtendedActivitySample(schema, user, device);
|
||||||
addHuamiStressSample(schema, user, device);
|
addHuamiStressSample(schema, user, device);
|
||||||
@@ -340,6 +341,19 @@ public class GBDaoGenerator {
|
|||||||
return activitySample;
|
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) {
|
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
|
||||||
Entity activitySample = addEntity(schema, "MiBandActivitySample");
|
Entity activitySample = addEntity(schema, "MiBandActivitySample");
|
||||||
activitySample.implementsSerializable();
|
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.OppoEncoAir2Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoBuds2Coordinator;
|
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.pebble.PebbleCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.pixel.PixelBudsACoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.pixel.PixelBudsACoordinator;
|
||||||
@@ -569,6 +570,7 @@ public enum DeviceType {
|
|||||||
MICOMPOSITIONSCALE(MiCompositionScaleCoordinator.class),
|
MICOMPOSITIONSCALE(MiCompositionScaleCoordinator.class),
|
||||||
BFH16(BFH16DeviceCoordinator.class),
|
BFH16(BFH16DeviceCoordinator.class),
|
||||||
MAKIBESHR3(MakibesHR3Coordinator.class),
|
MAKIBESHR3(MakibesHR3Coordinator.class),
|
||||||
|
OVTOUCH26(OVTouch26Coordinator.class),
|
||||||
BANGLEJS(BangleJSCoordinator.class),
|
BANGLEJS(BangleJSCoordinator.class),
|
||||||
FOSSILQHYBRID(QHybridCoordinator.class),
|
FOSSILQHYBRID(QHybridCoordinator.class),
|
||||||
TLW64(TLW64Coordinator.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
|
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, Damien
|
||||||
Gaignon, Daniele Gobbetti, José Rebelo, Petr Vaněk
|
Gaignon, Daniele Gobbetti, José Rebelo, Petr Vaněk, vappster
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@@ -73,7 +73,10 @@ public class CheckSums {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int getCRC16ansi(byte[] seq) {
|
public static int getCRC16ansi(byte[] seq) {
|
||||||
int crc = 0xffff;
|
return getCRC16ansi(seq, 0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getCRC16ansi(byte[] seq, int crc) {
|
||||||
int polynomial = 0xA001;
|
int polynomial = 0xA001;
|
||||||
|
|
||||||
for (int i = 0; i < seq.length; i++) {
|
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_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_mijia_mho_c303" translatable="false">Mijia MHO-C303</string>
|
||||||
<string name="devicetype_makibes_hr3" translatable="false">Makibes HR3</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_banglejs" translatable="false">Bangle.js</string>
|
||||||
<string name="devicetype_tlw64" translatable="false">TLW64</string>
|
<string name="devicetype_tlw64" translatable="false">TLW64</string>
|
||||||
<string name="devicetype_pinetime_jf" translatable="false">PineTime (JF Firmware)</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("M4-4711", DeviceType.FITPRO);
|
||||||
put("C20", DeviceType.C20); // #4070
|
put("C20", DeviceType.C20); // #4070
|
||||||
put("C 20", DeviceType.C20); // #5495
|
put("C 20", DeviceType.C20); // #5495
|
||||||
|
put("OV-Touch2.6_LE", DeviceType.OVTOUCH26);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user