mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-25 08:05:55 +01:00
Da Fit: Add device support, reverse engineering notes and base protocol implementation
This commit is contained in:
parent
c0883de546
commit
ca7d9e19af
@ -0,0 +1,258 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
|
||||
public class DaFitConstants {
|
||||
// (*) - based only on static reverse engineering of the original app code,
|
||||
// not supported by my watch so not implemented
|
||||
// (or at least I didn't manage to get any response out of it)
|
||||
|
||||
// (?) - not checked
|
||||
|
||||
|
||||
// The device communicates by sending packets by writing to UUID_CHARACTERISTIC_DATA_OUT
|
||||
// in MTU-sized chunks. The value of MTU seems to be somehow changeable (?), but the default
|
||||
// is 20. Responses are received via notify on UUID_CHARACTERISTIC_DATA_IN in similar format.
|
||||
// The write success notification comes AFTER the responses.
|
||||
|
||||
// Packet format:
|
||||
// packet[0] = 0xFE;
|
||||
// packet[1] = 0xEA;
|
||||
// if (MTU == 20) // could be a protocol version check?
|
||||
// {
|
||||
// packet[2] = 16;
|
||||
// packet[3] = packet.length;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// packet[2] = 32 + (packet.length >> 8) & 0xFF;
|
||||
// packet[3] = packet.length & 0xFF;
|
||||
// }
|
||||
// packet[4] = packetType;
|
||||
// packet[5:] = payload;
|
||||
|
||||
// Protocol version is determined by reading manufacturer name. MOYOUNG for old fixed-size
|
||||
// or MOYOUNG-V2 for MTU. The non-MTU version uses packets of size 256
|
||||
// for firmware >= 1.6.5, and 64 otherwise.
|
||||
|
||||
// The firmware version is also used to detect availability of some features.
|
||||
|
||||
// Additionally, there seems to be a trace of special packets with cmd 1 and 2, that are sent
|
||||
// to UUID_CHARACTERISTIC_DATA_SPECIAL_1 and UUID_CHARACTERISTIC_DATA_SPECIAL_2 instead.
|
||||
// They don't appear on my watch though.
|
||||
|
||||
// The response to CMD_ECG is special and is returned using UUID_CHARACTERISTIC_DATA_ECG_OLD
|
||||
// or UUID_CHARACTERISTIC_DATA_ECG_NEW. The old version is clearly labeled as old in the
|
||||
// unobfuscated part of the code. If both of them exist, old is used (but I presume only one
|
||||
// of them is supposed to exist at a time). They also don't appear on my watch as it doesn't
|
||||
// support ECG.
|
||||
|
||||
// In addition to the proprietary protocol described above, the following standard BLE services
|
||||
// are used:
|
||||
// * org.bluetooth.service.generic_access for device name
|
||||
// * org.bluetooth.service.device_information for manufacturer, model, serial number and
|
||||
// firmware version
|
||||
// * org.bluetooth.service.battery_service for battery level
|
||||
// * org.bluetooth.service.heart_rate is exposed, but doesn't seem to work
|
||||
// * org.bluetooth.service.human_interface_device is exposed, but not even mentioned
|
||||
// in the official app (?) - needs further research
|
||||
// * the custom UUID_CHARACTERISTIC_STEPS is used to sync the pedometer data in real time
|
||||
// via READ or NOTIFY - it's identical to the "sync past data" packet
|
||||
// ({distance:uint24, steps:uint24, calories:uint24})
|
||||
// * (?) 0000FEE7-0000-1000-8000-00805F9B34FB another custom service
|
||||
// (NOT UUID_CHARACTERISTIC_DATA_ECG_OLD!!!) not mentioned anywhere in the official app,
|
||||
// containing the following characteristics:
|
||||
// * 0000FEA1-0000-1000-8000-00805F9B34FB - READ, NOTIFY
|
||||
// * 0000FEC9-0000-1000-8000-00805F9B34FB - READ
|
||||
|
||||
// The above standard services are internally handled by the app using the following
|
||||
// "packet numbers":
|
||||
// * 16 - query steps
|
||||
// * 17 - firmware version
|
||||
// * 18 - query battery
|
||||
// * 19 - DFU status (queries model number, looks for the string DFU and a number == 0 or != 0)
|
||||
// * 20 - protocol version (queries manufacturer name, see description above)
|
||||
|
||||
|
||||
public static final UUID UUID_SERVICE_DAFIT = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "feea"));
|
||||
public static final UUID UUID_CHARACTERISTIC_STEPS = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee1"));
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_OUT = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee2"));
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_IN = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee3"));
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_SPECIAL_1 = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee5")); // (*)
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_SPECIAL_2 = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee6")); // (*)
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_ECG_OLD = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee7")); // (*)
|
||||
public static final UUID UUID_CHARACTERISTIC_DATA_ECG_NEW = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee8")); // (*)
|
||||
|
||||
|
||||
// Special
|
||||
public static final byte CMD_SHUTDOWN = 81; // {-1}
|
||||
public static final byte CMD_FIND_MY_WATCH = 97; // {}
|
||||
public static final byte CMD_FIND_MY_PHONE = 98; // (*) outgoing {-1} to stop, incoming {0} start, {!=0} stop
|
||||
public static final byte CMD_HS_DFU = 99; // (?) {1} - enableHsDfu(), {0} - queryHsDfuAddress()
|
||||
|
||||
|
||||
// Activity tracking (?)
|
||||
public static final byte CMD_QUERY_LAST_DYNAMIC_RATE = 52; // (?) {} -> ???
|
||||
public static final byte CMD_QUERY_PAST_HEART_RATE_1 = 53; // (?) {4} - pastHeartRate(), {0} - todayHeartRate(1) -> ???
|
||||
public static final byte CMD_QUERY_PAST_HEART_RATE_2 = 54; // (?) {0} - todayHeartRate(2) -> ???
|
||||
public static final byte CMD_QUERY_MOVEMENT_HEART_RATE = 55; // (?) {} -> ???
|
||||
|
||||
// Health measurements
|
||||
public static final byte CMD_QUERY_TIMING_MEASURE_HEART_RATE = 47; // (*) {} -> ???
|
||||
public static final byte CMD_SET_TIMING_MEASURE_HEART_RATE = 31; // (*) {i}, i >= 0, 0 is disabled
|
||||
public static final byte CMD_START_STOP_MEASURE_DYNAMIC_RATE = 104; // (*) {enabled ? 0 : -1}
|
||||
|
||||
public static final byte CMD_TRIGGER_MEASURE_BLOOD_PRESSURE = 105; // (?) {0, 0, 0} to start, {-1, -1, -1} to stop -> {unused?, num1, num2}
|
||||
public static final byte CMD_TRIGGER_MEASURE_BLOOD_OXYGEN = 107; // (?) {start ? 0 : -1} -> {num}
|
||||
public static final byte CMD_TRIGGER_MEASURE_HEARTRATE = 109; // {start ? 0 : -1} -> {bpm}
|
||||
public static final byte CMD_ECG = 111; // (?) {heart_rate} or {1} to start or {0} to stop or {2} to query
|
||||
// ECG data is special and comes from UUID_CHARACTERISTIC_DATA_ECG_OLD or UUID_CHARACTERISTIC_DATA_ECG_NEW
|
||||
|
||||
|
||||
// Functionality
|
||||
public static final byte CMD_SYNC_TIME = 49; // {time >> 24, time >> 16, time >> 8, time, 8}, time is a timestamp in seconds in GMT+8
|
||||
|
||||
public static final byte CMD_SYNC_SLEEP = 50; // {} -> {type, start_h, start_m}, repeating, type is SOBER(0),LIGHT(1),RESTFUL(2)
|
||||
public static final byte CMD_SYNC_PAST_SLEEP_AND_STEP = 51; // {b (see below)} -> {x<=2, distance:uint24, steps:uint24, calories:uint24} or {x>2, (sleep data like above)} - two functions same CMD
|
||||
|
||||
public static final byte ARG_SYNC_YESTERDAY_STEPS = 1;
|
||||
public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS = 2;
|
||||
public static final byte ARG_SYNC_YESTERDAY_SLEEP = 3;
|
||||
public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP = 4;
|
||||
|
||||
public static final byte SLEEP_SOBER = 0;
|
||||
public static final byte SLEEP_LIGHT = 1;
|
||||
public static final byte SLEEP_RESTFUL = 2;
|
||||
|
||||
public static final byte CMD_QUERY_SLEEP_ACTION = 58; // (*) {i} -> {hour, x[60]}
|
||||
|
||||
public static final byte CMD_SEND_MESSAGE = 65; // {type, message[]}, message is encoded with manual splitting by String.valueOf(0x2080)
|
||||
// CMD_SEND_CALL_OFF_HOOK = 65; // {-1} - the same ID as above, different arguments
|
||||
|
||||
public static final byte CMD_SET_WEATHER_FUTURE = 66; // {weatherId, low_temp, high_temp} * 7
|
||||
public static final byte CMD_SET_WEATHER_TODAY = 67; // {have_pm25 ? 1 : 0, weatherId, temp[, pm25 >> 8, pm25], lunar_or_festival[8], city[8]}, names are UTF-16BE encoded (4 characters each!)
|
||||
|
||||
public static final byte CMD_GSENSOR_CALIBRATION = 82; // (?) {}
|
||||
|
||||
public static final byte CMD_QUERY_STEPS_CATEGORY = 89; // (*) {i} -> {0, data:uint16[*]}, {1}, {2, data:uint16[*]}, {3}, query 0+1 together and 2+3 together
|
||||
//public static final byte ARG_QUERY_STEPS_CATEGORY_TODAY_STEPS = 0;
|
||||
//public static final byte ARG_QUERY_STEPS_CATEGORY_YESTERDAY_STEPS = 2;
|
||||
|
||||
public static final byte CMD_SWITCH_CAMERA_VIEW = 102; // {} -> {}, outgoing open screen, incoming take photo
|
||||
|
||||
public static final byte CMD_NOTIFY_PHONE_OPERATION = 103; // ONLY INCOMING! -> {x}, x -> 0 = play/pause, 1 = prev, 2 = next, 3 = reject incoming call)
|
||||
public static final byte CMD_NOTIFY_WEATHER_CHANGE = 100; // (?) ONLY INCOMING! -> {}
|
||||
|
||||
public static final byte ARG_OPERATION_PLAY_PAUSE = 0;
|
||||
public static final byte ARG_OPERATION_PREV_SONG = 1;
|
||||
public static final byte ARG_OPERATION_NEXT_SONG = 2;
|
||||
public static final byte ARG_OPERATION_DROP_INCOMING_CALL = 3;
|
||||
|
||||
public static final byte CMD_QUERY_ALARM_CLOCK = 33; // (?) {} -> a list of entries like below
|
||||
public static final byte CMD_SET_ALARM_CLOCK = 17; // (?) {id, enable ? 1 : 0, repeat, hour, minute, i >> 8, i, repeatMode}, repeatMode is 0(SINGLE), 127(EVERYDAY), or bitmask of 1,2,4,8,16,32,64(SUNDAY-SATURDAY) is 0,1,2, i is ((year << 12) + (month << 8) + day) for repeatMode=SINGLE and 0 otherwise, repeat is 0(SINGLE),1(EVERYDAY),2(OTHER)
|
||||
|
||||
// Settings
|
||||
public static final byte CMD_SET_USER_INFO = 18; // (?) {height, weight, age, gender}, MALE = 0, FEMALE = 1
|
||||
|
||||
public static final byte CMD_QUERY_DOMINANT_HAND = 36; // (*) {} -> {value}
|
||||
public static final byte CMD_SET_DOMINANT_HAND = 20; // (*) {value}
|
||||
|
||||
public static final byte CMD_QUERY_DISPLAY_DEVICE_FUNCTION = 37; // (*) {} - current, {-1} - list all supported -> {[-1, ], ...} (prefixed with -1 if lists supported, nothing otherwise)
|
||||
public static final byte CMD_SET_DISPLAY_DEVICE_FUNCTION = 21; // (*) {..., 0} - null terminated list of functions to enable
|
||||
|
||||
public static final byte CMD_QUERY_GOAL_STEP = 38; // {} -> {value, value >> 8, value >> 16, value >> 24} // this has the endianness swapped between query and set
|
||||
public static final byte CMD_SET_GOAL_STEP = 22; // {value >> 24, value >> 16, value >> 8, value} // yes, really
|
||||
|
||||
public static final byte CMD_QUERY_TIME_SYSTEM = 39; // {} -> {value}
|
||||
public static final byte CMD_SET_TIME_SYSTEM = 23; // {value}
|
||||
|
||||
// quick view = enable display when wrist is lifted
|
||||
public static final byte CMD_QUERY_QUICK_VIEW = 40; // {} -> {value}
|
||||
public static final byte CMD_SET_QUICK_VIEW = 24; // {enabled ? 1 : 0}
|
||||
|
||||
public static final byte CMD_QUERY_DISPLAY_WATCH_FACE = 41; // {} -> {value}
|
||||
public static final byte CMD_SET_DISPLAY_WATCH_FACE = 25; // {value}
|
||||
|
||||
public static final byte CMD_QUERY_METRIC_SYSTEM = 42; // {} -> {value}
|
||||
public static final byte CMD_SET_METRIC_SYSTEM = 26; // {value}
|
||||
|
||||
public static final byte CMD_QUERY_DEVICE_LANGUAGE = 43; // {} -> {value, bitmask_of_supported_langs:uint32}
|
||||
public static final byte CMD_SET_DEVICE_LANGUAGE = 27; // {new_value}
|
||||
|
||||
// enables "other" (as in "not a messaging app") on the notifications configuration screen in the official app
|
||||
// seems to be used only in the app, not sure why they even store it on the watch
|
||||
public static final byte CMD_QUERY_OTHER_MESSAGE_STATE = 44; // {} -> {value}
|
||||
public static final byte CMD_SET_OTHER_MESSAGE_STATE = 28; // {enabled ? 1 : 0}
|
||||
|
||||
public static final byte CMD_QUERY_SEDENTARY_REMINDER = 45; // {} -> {value}
|
||||
public static final byte CMD_SET_SEDENTARY_REMINDER = 29; // {enabled ? 1 : 0}
|
||||
|
||||
public static final byte CMD_QUERY_DEVICE_VERSION = 46; // {} -> {value}
|
||||
public static final byte CMD_SET_DEVICE_VERSION = 30; // {new_value}
|
||||
|
||||
public static final byte CMD_QUERY_WATCH_FACE_LAYOUT = 57; // (*) {} -> {time_position, time_top_content, time_bottom_content, text_color >> 8, text_color, background_picture_md5[32]}
|
||||
public static final byte CMD_SET_WATCH_FACE_LAYOUT = 56; // (*) {time_position, time_top_content, time_bottom_content, text_color >> 8, text_color, background_picture_md5[32]}, text_color is R5G6B5, background_picture is stored as hex digits (numbers 0-15 not chars '0'-'F' !)
|
||||
|
||||
public static final byte CMD_SET_STEP_LENGTH = 84; // (?) {value}
|
||||
|
||||
public static final byte CMD_QUERY_DO_NOT_DISTURB_TIME = -127; // {} -> {start >> 8, start, end >> 8, end} these are 16-bit values (somebody was drunk while writing this or what?)
|
||||
public static final byte CMD_SET_DO_NOT_DISTURB_TIME = 113; // {start_hour, start_min, end_hour, end_min}
|
||||
|
||||
public static final byte CMD_QUERY_QUICK_VIEW_TIME = -126; // {} -> {start >> 8, start, end >> 8, end} these are 16-bit values (somebody was drunk while writing this or what?)
|
||||
public static final byte CMD_SET_QUICK_VIEW_TIME = 114; // {start_hour, start_min, end_hour, end_min}
|
||||
|
||||
public static final byte CMD_QUERY_REMINDERS_TO_MOVE_PERIOD = -125; // {} -> {period, steps, start_hour, end_hour}
|
||||
public static final byte CMD_SET_REMINDERS_TO_MOVE_PERIOD = 115; // {period, steps, start_hour, end_hour}
|
||||
|
||||
public static final byte CMD_QUERY_SUPPORT_WATCH_FACE = -124; // (*) {} -> {count >> 8, count, ...}
|
||||
|
||||
public static final byte CMD_QUERY_PSYCHOLOGICAL_PERIOD = -123; // (*) {} -> ??? (too lazy to check, sorry :P)
|
||||
public static final byte CMD_SET_PSYCHOLOGICAL_PERIOD = 117; // (*) {encodeConfiguredReminders(info), 15, info.getPhysiologcalPeriod(), info.getMenstrualPeriod(), info.startDate.get(Calendar.MONTH), info.startDate.get(Calendar.DATE), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute()}
|
||||
// encodeConfiguredReminders(CRPPhysiologcalPeriodInfo info) {
|
||||
// int i = info.isMenstrualReminder() ? 241 : 240;
|
||||
// if (info.isOvulationReminder())
|
||||
// i += 2;
|
||||
// if (info.isOvulationDayReminder())
|
||||
// i += 4;
|
||||
// if (info.isOvulationEndReminder())
|
||||
// i += 8;
|
||||
// return (byte) i;
|
||||
// }
|
||||
|
||||
// no idea what this does
|
||||
public static final byte CMD_QUERY_BREATHING_LIGHT = -120; // {} -> {value}
|
||||
public static final byte CMD_SET_BREATHING_LIGHT = 120; // {enabled ? 1 : 0}
|
||||
|
||||
|
||||
public static final byte TRAINING_TYPE_WALK = 0;
|
||||
public static final byte TRAINING_TYPE_RUN = 1;
|
||||
public static final byte TRAINING_TYPE_BIKING = 2;
|
||||
public static final byte TRAINING_TYPE_ROPE = 3;
|
||||
public static final byte TRAINING_TYPE_BADMINTON = 4;
|
||||
public static final byte TRAINING_TYPE_BASKETBALL = 5;
|
||||
public static final byte TRAINING_TYPE_FOOTBALL = 6;
|
||||
public static final byte TRAINING_TYPE_SWIM = 7;
|
||||
public static final byte TRAINING_TYPE_MOUNTAINEERING = 8;
|
||||
public static final byte TRAINING_TYPE_TENNIS = 9;
|
||||
public static final byte TRAINING_TYPE_RUGBY = 10;
|
||||
public static final byte TRAINING_TYPE_GOLF = 11;
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.DAFIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Media-Tech";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ParcelUuid service = new ParcelUuid(DaFitConstants.UUID_SERVICE_DAFIT);
|
||||
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(service).build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
// TODO: It would be nice to also filter on "manufacturer" (which is used as a protocol version) being MOYOUNG-V2 or MOYOUNG but I have no idea if it's possible to do that at this point
|
||||
if (candidate.supportsService(DaFitConstants.UUID_SERVICE_DAFIT)) {
|
||||
return DeviceType.DAFIT;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMusicInfo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsLedColor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRgbLedColor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public int[] getColorPresets() {
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() { return false; }
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,362 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
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;
|
||||
|
||||
// TODO: figure out the training data
|
||||
|
||||
public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DaFitDeviceSupport.class);
|
||||
|
||||
private final DeviceInfoProfile<DaFitDeviceSupport> deviceInfoProfile;
|
||||
private final BatteryInfoProfile<DaFitDeviceSupport> batteryInfoProfile;
|
||||
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
private final IntentListener mListener = new IntentListener() {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
String s = intent.getAction();
|
||||
if (Objects.equals(s, DeviceInfoProfile.ACTION_DEVICE_INFO)) {
|
||||
handleDeviceInfo((DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
|
||||
}
|
||||
if (Objects.equals(s, BatteryInfoProfile.ACTION_BATTERY_INFO)) {
|
||||
handleBatteryInfo((BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static final int MTU = 20; // TODO: there seems to be some way to change this value...?
|
||||
private DaFitPacketIn packetIn = new DaFitPacketIn();
|
||||
|
||||
public DaFitDeviceSupport() {
|
||||
super(LOG);
|
||||
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
|
||||
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
|
||||
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
|
||||
addSupportedService(DaFitConstants.UUID_SERVICE_DAFIT);
|
||||
|
||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||
deviceInfoProfile.addListener(mListener);
|
||||
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||
batteryInfoProfile.addListener(mListener);
|
||||
addSupportedProfile(deviceInfoProfile);
|
||||
addSupportedProfile(batteryInfoProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
builder.notify(getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN), true);
|
||||
deviceInfoProfile.requestDeviceInfo(builder);
|
||||
batteryInfoProfile.requestBatteryInfo(builder);
|
||||
batteryInfoProfile.enableNotify(builder);
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private BluetoothGattCharacteristic getTargetCharacteristicForPacketType(byte packetType)
|
||||
{
|
||||
if (packetType == 1)
|
||||
return getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_DATA_SPECIAL_1);
|
||||
else if (packetType == 2)
|
||||
return getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_DATA_SPECIAL_2);
|
||||
else
|
||||
return getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_DATA_OUT);
|
||||
}
|
||||
|
||||
public void sendPacket(TransactionBuilder builder, byte[] packet)
|
||||
{
|
||||
DaFitPacketOut packetOut = new DaFitPacketOut(packet);
|
||||
|
||||
byte[] fragment = new byte[MTU];
|
||||
while(packetOut.getFragment(fragment))
|
||||
{
|
||||
builder.write(getTargetCharacteristicForPacketType(packet[4]), fragment.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
UUID charUuid = characteristic.getUuid();
|
||||
if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN))
|
||||
{
|
||||
if (packetIn.putFragment(characteristic.getValue())) {
|
||||
Pair<Byte, byte[]> packet = DaFitPacketIn.parsePacket(packetIn.getPacket());
|
||||
packetIn = new DaFitPacketIn();
|
||||
if (packet != null) {
|
||||
byte packetType = packet.first;
|
||||
byte[] payload = packet.second;
|
||||
|
||||
Log.i("AAAAAAAAAAAAAAAA", "Response for: " + packetType);
|
||||
|
||||
if (handlePacket(packetType, payload))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
|
||||
private boolean handlePacket(byte packetType, byte[] payload)
|
||||
{
|
||||
LOG.warn("Got packet " + packetType + ": " + Logging.formatBytes(payload));
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleDeviceInfo(DeviceInfo info) {
|
||||
LOG.warn("Device info: " + info);
|
||||
versionCmd.hwVersion = info.getHardwareRevision();
|
||||
versionCmd.fwVersion = info.getSoftwareRevision();
|
||||
handleGBDeviceEvent(versionCmd);
|
||||
}
|
||||
|
||||
private void handleBatteryInfo(BatteryInfo info) {
|
||||
LOG.warn("Battery info: " + info);
|
||||
batteryCmd.level = (short) info.getPercentCharged();
|
||||
handleGBDeviceEvent(batteryCmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
// not supported :(
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
// not supported :(
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
// TODO: set alarms
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
// not supported :(
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
// not supported :(
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(UUID uuid, boolean start) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(UUID uuid) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(UUID[] uuids) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
// TODO: this shuts down the watch, rather than rebooting it - perhaps add a new operation type?
|
||||
// (reboot is not supported, btw)
|
||||
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("shutdown");
|
||||
sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SHUTDOWN, new byte[] { -1 }));
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// TODO: starting other tests
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(boolean enable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
if (start)
|
||||
{
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("onFindDevice");
|
||||
sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_FIND_MY_WATCH, new byte[0]));
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not supported - the device vibrates three times and then stops automatically
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int integer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadConfiguration(String config) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFmFrequency(float frequency) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetLedColor(int color) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
public class DaFitPacket {
|
||||
protected byte[] packet;
|
||||
protected int position = 0;
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
|
||||
/**
|
||||
* A class for handling fragmentation of incoming packets<br>
|
||||
* <br>
|
||||
* Usage:
|
||||
* <pre>
|
||||
* {@code
|
||||
* if(packetIn.putFragment(fragment)) {
|
||||
* Pair<Byte, byte[]> packet = DaFitPacketIn.parsePacket(packetIn.getPacket());
|
||||
* packetIn = new DaFitPacketIn();
|
||||
* if (packet != null) {
|
||||
* byte packetType = packet.first;
|
||||
* byte[] payload = packet.second;
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public class DaFitPacketIn extends DaFitPacket {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DaFitPacketIn.class);
|
||||
|
||||
public DaFitPacketIn()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the incoming fragment and try to reconstruct packet
|
||||
*
|
||||
* @param fragment The incoming fragment
|
||||
* @return true if the packet is complete
|
||||
*/
|
||||
public boolean putFragment(byte[] fragment)
|
||||
{
|
||||
if (packet == null)
|
||||
{
|
||||
int len = parsePacketLength(fragment);
|
||||
if (len < 0)
|
||||
return false; // corrupted packet
|
||||
packet = new byte[len];
|
||||
}
|
||||
|
||||
int toCopy = Math.min(fragment.length, packet.length - position);
|
||||
if (fragment.length > toCopy)
|
||||
{
|
||||
LOG.warn("Got fragment with more data than expected!");
|
||||
}
|
||||
|
||||
System.arraycopy(fragment, 0, packet, position, toCopy);
|
||||
position += fragment.length;
|
||||
return position >= packet.length;
|
||||
}
|
||||
|
||||
public byte[] getPacket()
|
||||
{
|
||||
if (packet == null || position < packet.length)
|
||||
throw new IllegalStateException("Packet is not complete yet");
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the packet header and return the length
|
||||
* @param packetOrFragment The entire packet or it's first fragment
|
||||
* @return The packet length, or -1 if packet is corrupted
|
||||
*/
|
||||
private static int parsePacketLength(@NonNull byte[] packetOrFragment)
|
||||
{
|
||||
if (packetOrFragment[0] != (byte)0xFE || packetOrFragment[1] != (byte)0xEA)
|
||||
{
|
||||
LOG.warn("Invalid packet header, ignoring! Fragment: " + Logging.formatBytes(packetOrFragment));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int len_h = 0;
|
||||
if (packetOrFragment[2] != 16)
|
||||
{
|
||||
if ((packetOrFragment[2] & 0xFF) < 32)
|
||||
{
|
||||
LOG.warn("Corrupted packet, unable to parse length");
|
||||
return -1;
|
||||
}
|
||||
len_h = (packetOrFragment[2] & 0xFF) - 32;
|
||||
}
|
||||
int len_l = (packetOrFragment[3] & 0xFF);
|
||||
|
||||
return (len_h << 8) | len_l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the packet
|
||||
* @param packet The complete packet
|
||||
* @return A pair containing the packet type and payload
|
||||
*/
|
||||
public static Pair<Byte, byte[]> parsePacket(@NonNull byte[] packet)
|
||||
{
|
||||
int len = parsePacketLength(packet);
|
||||
if (len < 0)
|
||||
return null;
|
||||
if (len != packet.length)
|
||||
{
|
||||
LOG.warn("Invalid packet length!");
|
||||
return null;
|
||||
}
|
||||
byte packetType = packet[4];
|
||||
byte[] payload = new byte[packet.length - 5];
|
||||
System.arraycopy(packet, 5, payload, 0, payload.length);
|
||||
return Pair.create(packetType, payload);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/* Copyright (C) 2019 krzys_h
|
||||
|
||||
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.dafit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A class for handling fragmentation of outgoing packets<br>
|
||||
* <br>
|
||||
* Usage:
|
||||
* <pre>
|
||||
* {@code
|
||||
* DaFitPacketOut packetOut = new DaFitPacketOut(DaFitPacketOut.buildPacket(type, payload));
|
||||
* byte[] fragment = new byte[MTU];
|
||||
* while(packetOut.getFragment(fragment))
|
||||
* send(fragment);
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public class DaFitPacketOut extends DaFitPacket {
|
||||
public DaFitPacketOut(byte[] packet)
|
||||
{
|
||||
this.packet = packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next fragment of this packet to be sent
|
||||
*
|
||||
* @param fragmentBuffer The buffer to store the output in, of desired size (i.e. == MTU)
|
||||
* @return true if there is more data to be sent, false otherwise
|
||||
*/
|
||||
public boolean getFragment(byte[] fragmentBuffer)
|
||||
{
|
||||
if (position >= packet.length)
|
||||
return false;
|
||||
int remainingToTransfer = Math.min(fragmentBuffer.length, packet.length - position);
|
||||
System.arraycopy(packet, position, fragmentBuffer, 0, remainingToTransfer);
|
||||
position += remainingToTransfer;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the packet
|
||||
* @param packetType The packet type
|
||||
* @param payload The packet payload
|
||||
* @return The encoded packet
|
||||
*/
|
||||
public static byte[] buildPacket(byte packetType, @NonNull byte[] payload)
|
||||
{
|
||||
byte[] packet = new byte[payload.length + 5];
|
||||
packet[0] = (byte)0xFE;
|
||||
packet[1] = (byte)0xEA;
|
||||
if (DaFitDeviceSupport.MTU == 20)
|
||||
{
|
||||
packet[2] = 16;
|
||||
packet[3] = (byte)(packet.length & 0xFF);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet[2] = (byte)(32 + (packet.length >> 8) & 0xFF);
|
||||
packet[3] = (byte)(packet.length & 0xFF);
|
||||
}
|
||||
packet[4] = packetType;
|
||||
System.arraycopy(payload, 0, packet, 5, payload.length);
|
||||
return packet;
|
||||
}
|
||||
}
|
@ -1829,6 +1829,7 @@
|
||||
<string name="devicetype_mijia_lywsd03">Mijia Temperature and Humidity Sensor 2</string>
|
||||
<string name="devicetype_mijia_xmwsdj04">Mijia Temperature and Humidity Sensor 2 (E-ink)</string>
|
||||
<string name="devicetype_mijia_mho_c303">Mijia MHO-C303</string>
|
||||
<string name="devicetype_dafit">Da Fit</string>
|
||||
<string name="devicetype_makibes_hr3">Makibes HR3</string>
|
||||
<string name="devicetype_banglejs">Bangle.js</string>
|
||||
<string name="devicetype_tlw64">TLW64</string>
|
||||
|
Loading…
Reference in New Issue
Block a user