Table of Contents
- Supporting a new device in Gadgetbridge
- Part 1: Discovery
- Part 2: Communication
- Bluetooth Classic
- Bluetooth LE
- Part 3: Initialization
- Part 4. Receiving data
- Part 4: Activity Databases
- Defining a Database
- Creating a sample provider
- Writing data to your database
- Fetching of sports data
- Handle deleting of the data when the user removes the device from Gadgetbridge
- Part 5: Per-user-device settings
- Note about translations
- Adding list of supported settings to the device coordinator class
- Accessing settings from your device's support class
- Reacting to changes on the fly
- Part 6: Data about the user
- Final Words
Supporting a new device in Gadgetbridge
This is a step by step tutorial for supporting a new device in Gadgetbridge. There are some differences depending on the transport layer being used (Bluetooth Classic, Bluetooth LE, WiFi, ...), but most of it is independent of that. The tutorial only covers the basics to get you started quickly. For more details, have a look at the existing implementations for other devices. Try to do your implementation in a similar way (where it makes sense), so as to ease maintenance.
Here you'll find some information how to reverse engineer the BT protocol of your device: BT Protocol Reverse Engineering
See Appendix: Infrastructure for some general implementation topics and Developer-Documentation for an overview into Gadgetbridge's inner workings and important classes.
Before going into the details, let's have a short look at the process the user has to go through in order to connect a new device.
Start Device Discovery -> Select one of the displayed devices -> If necessary: pair/authenticate with the device -> If necessary: provide certain settings like nick name, age, height, ... -> Initialize the device -> Device can be used
For this tutorial we will add support for the exemplary SuperBand 4000 device, so many things are named after that.
Part 1: Discovery
The first part deals with discovering SuperBand 4000 devices and telling Gadgetbridge that/how they are supported.
-
Add a new device type constant SUPERBAND_4000 to
nodomain.freeyourgadget.gadgetbridge.model.DeviceType
. Pick a new number as thekey
and leave 10 numbers "room" to the last number. These are reserved so that related devices can have nearby numbers. Also specify a default icon (coloured) and a disabled icon (greyscale). If you don't have any yet, reuse some other icons.SUPERBAND_4000(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, "SuperBand 4000");
-
Create a new package
nodomain.freeyourgadget.gadgetbridge.devices.superband4000
. -
In that package, create a class
SuperBand4000DeviceCoordinator
that extendsnodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator
. This class tells Gadgetbridge whether a discovered device is supported and what features are supported (with the implementation in Gadgetbridge, not in general).public class SuperBand4000DeviceCoordinator extends AbstractDeviceCoordinator { }
- Implement the method
getDeviceType()
and return the constantDeviceType.SUPERBAND_4000
that you previously added.
@Override public DeviceType getDeviceType() { return DeviceType.SUPERBAND_4000; }
- Implement the method
getManufacturer()
and return the manufacturer name.
@Override public String getManufacturer() { return "SuperManufacturer"; }
- Implement the method
getDeviceSupportClass
to return the support class. This class will be implemented in Part 2, so for now we can return the UnknownDeviceSupport:
@NonNull @Override public Class<? extends DeviceSupport> getDeviceSupportClass() { return UnknownDeviceSupport.class; }
- If your device is a Bluetooth LE device, override the method
createBLEScanFilters()
and return a list ofScanFilter
s for recognizing your device during discovery by a Bluetooth service UUID for example. You can skip this if you cannot or don't know how to recognize your device that way yet.
@NonNull @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Collection<? extends ScanFilter> createBLEScanFilters() { }
- Implement the method
DeviceType getSupportedType(GBDeviceCandidate candidate)
. This method will be called during discovery in order to check whether the discovered device is supported by Gadgetbridge. To decide this, you might check further services, the MAC address, or even the device name if you don't know a better way, yet. If you're determined to support the given device candidate, you return the recognizedDeviceType
. Typically this is the one that you return ingetDeviceType()
. If you do not recognize the given candidate, returnDeviceType.UNKNOWN
so that anotherDeviceCoordinator
can be asked.
@NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { if (candidate.supportsService(SUPERBAND_4000Constants.UUID_SERVICE_SUPERBAND_4000)) { return DeviceType.SUPERBAND_4000; } return DeviceType.UNKNOWN; }
- Implement
getBondingStyle()
depending on how your device needs to be paired on the Bluetooth level. ReturnBONDING_STYLE_BOND
to perform BLE pairing,BONDING_STYLE_ASK
to let the user decide that during discovery, orBONDING_STYLE_NONE
to not perform BLE pairing at all.
It is better to use some bonding style like
BONDING_STYLE_BOND
instead ofBONDING_STYLE_NONE
, because that allows Gadgetbridge to reconnect a disconnected device in a much better way, without having to scan for the device.@Override public int getBondingStyle(){ return BONDING_STYLE_NONE; }
- If your device needs some very special kind of pairing/bonding/authentication before it can be used, you may override the method
getPairingActivity()
in which you return anActivity
class that will be shown for your device after the device discovery process. Otherwise, just returnnull
.
@Nullable @Override public Class<? extends Activity> getPairingActivity() { return null; }
- There are several more methods that you need to implement, mostly
supportsXXX()
whereXXX
is a certain feature a device may support or not. As soon as you implement support for one such feature, make sure to announce it by returningtrue
in the correspondingsupportsXXX()
method. Otherwise the feature will not be available in Gadgetbridge.
@Override public boolean supportsWeather() { return false; } @Override public boolean supportsFindDevice() { return false; }
- Register your
SuperBand4000DeviceCoordinator
class withnodomain.freeyourgadget.gadgetbridge.util.DeviceHelper#createCoordinators()
so that it will be used.
private List<DeviceCoordinator> createCoordinators() { List<DeviceCoordinator> result = new ArrayList<>(); result.add(new SuperBand4000DeviceCoordinator()); }
- Implement the method
So, that was the first part. With these changes, Gadgetbridge should be able to find and display your device in the Discovery view. If that is not the case, add breakpoints in your device coordinator's getSupportedType()
method to find out why. Also check the ScanFilter
s, if any. Only once your device is found and displayed during discovery, does it make sense to continue with the next steps. To make the device being displayed by Gadgetbridge, Initialization must be done, as per below.
Part 2: Communication
The next part deals with the communication between Gadgetbridge and your device. This part is very specific to the device and the communication protocol being used.
This code lives in a separate package because it is used by a continuously running background Service
. This service is the nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService
.
- Create the package
nodomain.freeyourgadget.gadgetbridge.service.devices.superband4000
- Create a
SuperBand4000DeviceSupport
class in that package. As the super class, choose one of the existing abstract classes likenodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport
ornodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport
, depending on the transport layer. - Edit
SuperBand4000DeviceCoordinator#getDeviceSupportClass()
and return the newSuperBand4000DeviceSupport.class
.
Do note that some fields in DeviceAttributes entity are mandatory, so you must have to set it to an empty string if needed. See details here.
Bluetooth Classic
For Bluetooth Classic with AbstractSerialDeviceSupport
as super class, you will usually create at least two other classes: a SuperBand4000IOThread
and a SuperBand4000Protocol
.
- The
*IOThread
class manages the Bluetooth rfcomm socket in a non-blocking way for callers. - The
*Protocol
class understands and handles the incoming and outgoing messages.
Bluetooth LE
For Bluetooth LE, you will use the nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction
and nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue
classes to communicate with the device via GATT events. Theses classes will take care of synchronizing everything.
See the subclasses of nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile
for some ready-to-be-used implementations of standard BLE services/profiles.
Part 3: Initialization
After pairing, your device will have to be marked as "initialized" to be displayed by Gadgetbridge. SuperBand4000DeviceSupport
is responsible for this implementation.
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
// mark the device as initializing
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
// ... custom initialization logic ...
// set device firmware to prevent the following error when you (later) try to save data to database and
// device firmware has not been set yet
// Error executing 'the bind value at index 2 is null'java.lang.IllegalArgumentException: the bind value at index 2 is null
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
// mark the device as initialized
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
return builder;
}
Part 4. Receiving data
During the initialization and connection, service discovery is performed and list of services can be seen in the logs:
discovered unsupported service: Unknown Service: 00010203-0405-0607-0809-0a0b0c0d1912
Many GATT services are already defined in the GattService (GattService.UUID_SERVICE_GENERIC_ACCESS
) or in other places so search for the UUID in the codebase, most likely it is already defined.
To use services to be able to process their data, you must register them with the addSupportedService()
in the SuperBand4000DeviceSupport
constructor and then subscribe via builder.notify()
in the initializeDevice
, the onCharacteristicChanged()
will then be the place where to process the data, for example like this:
public SuperBand4000DeviceSupport() {
super(LOG);
addSupportedService(SuperBand4000Constants.SOME_UUID);
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
setInitialized(builder);
builder.notify(getCharacteristic(SuperBand4000Constants.SOME_UUID), true);
You can then observe this data and start processing:
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
UUID characteristicUUID = characteristic.getUuid();
byte[] value = characteristic.getValue();
LOG.info("Characteristic changed UUID: " + characteristicUUID);
LOG.info("Characteristic changed value: " + characteristic.getValue());
return false;
}
Device Info and Battery via standard GATT services
Device info
If the device supports GattService.UUID_SERVICE_DEVICE_INFORMATION
, you can try to receive Device Information through the DeviceInfoProfile provided by Gadgetbridge:
Define class variable deviceInfoProfile
in the SuperBand4000DeviceSupport
class:
public class SuperBand4000DeviceSupport extends AbstractBTLEDeviceSupport {
private final DeviceInfoProfile<SuperBand4000DeviceSupport> deviceInfoProfile;
Add the supported service, Gadgetbridge's DeviceInfoProfile initialization and an Intent listener to the constructor:
public SuperBand4000DeviceSupport() {
super(LOG);
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((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
}
}
};
deviceInfoProfile = new DeviceInfoProfile<>(this);
deviceInfoProfile.addListener(mListener);
addSupportedProfile(deviceInfoProfile);
During initialization, request the device info:
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
deviceInfoProfile.requestDeviceInfo(builder);
And define the handleDeviceInfo
method, which will take care to process the received data:
private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
LOG.debug("Device info: " + info);
versionCmd.fwVersion = info.getFirmwareRevision();
handleGBDeviceEvent(versionCmd);
}
Battery info with a notification listener
If the device supports GattService.UUID_SERVICE_BATTERY_SERVICE
, you can try to receive Device Information through the BatteryInfoProfile provided by Gadgetbridge:
Define class variable batteryInfoProfile
in the SuperBand4000DeviceSupport
class:
public class SuperBand4000DeviceSupport extends AbstractBTLEDeviceSupport {
private final BatteryInfoProfile<SuperBand4000DeviceSupport> batteryInfoProfile;
Add the supported service, Gadgetbridge's DeviceInfoProfile initialization and an Intent listener to the constructor:
public SuperBand4000DeviceSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
IntentListener mListener = new IntentListener() {
@Override
public void notify(Intent intent) {
String action = intent.getAction();
if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(action)) {
handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
}
}
};
batteryInfoProfile = new BatteryInfoProfile<>(this);
batteryInfoProfile.addListener(mListener);
addSupportedProfile(batteryInfoProfile);
During initialization, request the device info and also enable notification, so battery is updated when the device announces it:
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
batteryInfoProfile.requestBatteryInfo(builder);
batteryInfoProfile.enableNotify(builder, true);
And define the handleBatteryInfo
method, which will take care to process the received data:
private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
LOG.debug("Battery info: " + info);
batteryCmd.level = (short) info.getPercentCharged();
handleGBDeviceEvent(batteryCmd);
}
After this, you have firmware information and battery information in Gadgetbridge.
Part 4: Activity Databases
Most Fitness devices provide per-minute data which contain things like intensity of movement, activity type (sleep, deep sleep, awake, ...) and heartrate. We store this data in database tables.
Defining a Database
Gadgetbridge uses greenDAO. That means we do not use sql directly, but generated classes which are defined here:
./GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
You can see that each device defines it's own table, some of them more then one. The reasons behind this are.
- Different devices provide different data (some provide HR data, some not), others may have an environment light sensor.
- Each device uses a different way of encoding the data
- Some devices provide data though additional data streams (eg. sleep overlay data with from-to timestamps)
- Most important: This allows us to store data as raw as possible and interpreting things on the fly. A good example are Huami devices which encode unknown flags which we store but do not know how to make sense of them, if we did we could properly interpret past data
Start by adding a new method (imagine the Super Band 4000 device again). The following would be appropriate for a device which has a step counter, HR, and activity types (sleep, deepsleep, awake,...)
Heart rate is normally in bpm, and steps are an absolute number, so these are pretty straight forward.
For activity types it is often more complicated because there are often custom or unknown values for different vendors. By keeping the raw value we prevent information loss: We can interpret the values on the fly and charts will be updated accordingly when the code that interprets the value will be updates.
Also the intensity has often different ranges, 0-255 or 0-15 for example. We want to keep them raw, they will be normalized by the Sample provider (see next section).
The method that defines the database table would look like:
private static Entity addSuperBand4000ActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "SuperBand4000ActivitySample");
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
add that that in
public static void main()
Do not forget to bump the schema version, if it was at 24, bump it to 25
Schema schema = new Schema(24, MAIN_PACKAGE + ".entities");
Now when you build Gadgetbridge entity classes will be generated and the database you defined will be created
Synthetically generate sample data on the fly
There might be cases when devices do not give you an "intensity", but you have something like steps and calories. Or you might get data which you do not fully understand and therefore just put them in a raw database field and want to be able to retroactively make sense of that data. In those cases you can put a layer in between sample classes.
- In GBDaoGenerator change
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
to
addCommonActivitySampleProperties("AbstractSuperBand4000ActivitySample", activitySample, user, device);
- Actually create the AbstractSuperBand4000ActivitySample class in the nodomain.freeyourgadget.gadgetbridge.entities package
package nodomain.freeyourgadget.gadgetbridge.entities;
public abstract class AbstractSuperBand4000ActivitySample extends AbstractActivitySample {
abstract public int getCalories();
abstract public int getSteps();
@Override
public int getRawIntensity() {
return getCalories()*getSteps()*specialExampleConstant;
}
}
Important: As this whole directory app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities
is excluded from git via local .gitignore
file, make sure to add the AbstractSuperBand4000ActivitySample.java
to git manually.
This is just a stupid example to show how to use both calories and steps. In any case you should know you minimum and maximum values so that you can adjust normalizeIntensity() in the sample provider accordingly. It could also be enough just to map calories to intensity.
This example is a bit unfortunate, because calories should handled in a generic way by Gadgetbridge.
Creating a sample provider
For our band the sample provider would look like the following.
Pay special attention to normalizeType()
and normalizeIntensity()
.
In our example the SuperBand4000 has different sleep types we do not handle yet, they will be mapped to something close that we support. Still no data is lost.
In normalizeIntensity() you we assume the band stores intensity in a range from 0-255, we normalize that to 0.0f-1.0f so that the charts work properly.
package nodomain.freeyourgadget.gadgetbridge.devices.superband4000;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.SuperBand4000ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.SuperBand4000ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class SuperBand4000SampleProvider extends AbstractSampleProvider<SuperBand4000ActivitySample> {
public SuperBand4000SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<SuperBand4000ActivitySample, ?> getSampleDao() {
return getSession().getSuperBand4000ActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return SuperBand4000ActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return SuperBand4000ActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return SuperBand4000ActivitySampleDao.Properties.DeviceId;
}
@Override
public int normalizeType(int rawType) {
switch (rawType) {
case 1: //DEEP_NAP
case 2: //DEEP_SLEEP
return ActivityKind.TYPE_DEEP_SLEEP;
case 3: //LIGHT_NAP
case 4: //LIGHT_SLEEP
return ActivityKind.TYPE_LIGHT_SLEEP;
case 5: //ACTIVITY
case 6: //WALK
case 7: //RUN
return ActivityKind.TYPE_ACTIVITY;
default:
return ActivityKind.TYPE_UNKNOWN;
}
}
@Override
public int toRawActivityKind(int activityKind) {
switch (activityKind) {
case ActivityKind.TYPE_ACTIVITY:
return 5; // ACTIVITY
case ActivityKind.TYPE_DEEP_SLEEP:
return 2; // DEEP_SLEEP
case ActivityKind.TYPE_LIGHT_SLEEP:
return 4; // LIGH_SLEEP
default:
return 5; //ACTIVITY
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / 255.0f;
}
@Override
public SuperBand4000ActivitySample createActivitySample() {
return new SuperBand4000ActivitySample();
}
}
Now you have to register that sample provider in your SuperBand4000Coordinator
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new SuperBand4000SampleProvider(device, session);
}
Now the Gadgetbridge charts activity is able to access your data, let's see how to get it into the database in the next section
Writing data to your database
Imagine your device sends you aggregated per-minute data in batches of 16 samples with a simple header containing the number of samples and an initial timestamp. The code would look something like that:
private void processSamples(byte[] data) {
ByteBuffer buf = ByteBuffer.wrap(data);
buf.order(ByteOrder.LITTLE_ENDIAN);
int samples = buf.get();
int timestamp = buf.getInt();
SuperBand4000Samples[] superBand4000Samples = new SuperBand4000Samples[samples];
try (DBHandler db = GBApplication.acquireDB()) {
SuperBand4000SampleProvider sampleProvider = new SuperBand4000Provider(device, db.getDaoSession());
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
for (int i = 0; i < samples; i++) {
int rawIntensity = buf.get() & 0xff;
int steps = buf.getShort();
int rawType = buf.get() & 0xff;
int hr = buf.get() & 0xff;
superBand4000Samples[i] = new SuperBand4000Sample(timestamp + i * 60, deviceId, userId, rawIntensity, steps, rawType, hr);
}
sampleProvider.addGBActivitySamples(superBand4000Samplers);
} catch (Exception e) {
LOG.error("Error acquiring database", e);
}
Fetching of sports data
Use the onFetchRecordedData
method in the SuperBand4000Coordinator
to define how to initialize the fetching operation of these step/sleep data.
@Override
public void onFetchRecordedData(int dataTypes) {
//define how to request data from the device
}
Use the nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus
to indicate the status of the operation, search the code for example usage in implementations of another bands. Also see the nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.java
setBusyTask
and `unsetBusyTask to mark the device as busy while performing a certain task. While busy, no other operations will be performed on the device.
Handle deleting of the data when the user removes the device from Gadgetbridge
When the user removes the device, make sure the remove the user data as well. That is handled in the SuperBand4000Coordinator
by the deleteDevice
method:
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getSuperBand4000ActivitySampleDao().queryBuilder();
qb.where(SuperBand4000ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
That's it!
If you have a device that does proper analysis for sleep on-device like the Pebble Time, which sends sleep data in a completely different format after a sleep session has ended for super, you will have to do some more work. TODO: explain how to do it.
Part 5: Per-user-device settings
Historically Gadgetbridge had device type
specific settings, which were
submenus in the global Settings activity. While there are still some
submenus like "Pebble" or "Mi Band/Amazfit" those are considered deprecated
and new devices should implement settings by relying on the per-user-device
settings "framework" which Gadgetbridge provides. Using the framework will
make a "settings" icon visible inside the device card in the Gadgetbridge
main screen. Consider these recommendations when adding new settings.
There are multiple reasons why we are switching to per-user-device settings:
-
A growing list of devices that are supported let the menu grow and it was already cluttered while most of the devices listed are completely uninteresting to users who only uses one single device
-
Per-user-device settings allows to configure devices of the same type in a different way, this is even necessary for paring keys that some newer devices require to be obtained before using Gadgetbridge.
-
Amazfit/Mi Band devices all have a different feature set, and people will not understand why they are presented options that won't work on their device.
Note about translations
Do not add translations by editing the language variants of strings.xml directly as this creates merge conflicts between Codeberg git repo and Weblate git repo. Always use Weblate, as per info in the wiki.
Adding list of supported settings to the device coordinator class
Technically you provide a list of xml snippets that are combined to a settings activity during runtime.
You can find those that are already present here in
./app/src/main/res/xml/devicesettings_*.xml
To specify which ones are supported for you device add a list in your SuperBand4000Coordinator
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_dateformat,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_pairingkey
};
}
In this example, your super band supports switching the date format, allows you to receive a disconnect notification if the Bluetooth connection drops, and a pairing key can be specified to use with the device. Those three settings will be combined into a settings activity during runtime automatically!
If you need a setting that is not available in any of the xml files, you have to create your own. But please keep in mind to use generic key names, do not prefix them with your device name or anything! Try to create xml files that can be reused for other devices. If you think you need settings that are unlikely being reused by other devices, create a devicesettings_superband4000.xml file where you collect those setting.
Customizing settings
Look at how some devices utilize getDeviceSpecificSettingsCustomizer
which allows to provide some device specific settings modifications without putting all the logic into a single global class.
Settings grouping
Device settings are grouped into three coordinators, in order not to have one super long settings screen:
getSupportedDeviceSpecificSettings
→ settings related to the device itselfgetSupportedDeviceSpecificApplicationSettings
→ device's settings that are related to the application (like charts, connections...)getSupportedDeviceSpecificAuthenticationSettings
→ if the device requires some special authentification key or something, add the preference here. On long tap on the device in the pairing screen, these settings will be made visible. Look at how this is used and you can even add some extra info screens, to make it more clear why these are needed or what needs to be done.
Accessing settings from your device's support class
To access a SharedPreferences object which is specific to the device in use there is a helper function in GBApplicaction
called getDeviceSpecificSharedPrefs()
.
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
No you can access the settings by the same key that was defined in the corresponding devicesettings_*xml file.
Reacting to changes on the fly
Most already defined preferences have a handler that will cause onSendConfiguration(String config)
to be called in your Support class.
For the example of the date format it can look like this:
public void onSendConfiguration(String config) {
TransactionBuilder builder;
switch (config) {
case DeviceSettingsPreferenceConst.PREF_DATEFORMAT:
String dateFormat = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("dateformat", "MM/dd/yyyy");
// send something to the watch
break;
}
If you created a new xml file with a new setting you can add a new change handler for the key you specified in the devicesetting_*xml file by adding one line to DeviceSpecificSettingsFragment.java
addPreferenceHandlerFor(PREF_MYNEWPREF);
Part 6: Data about the user
You may need to provide some information about the user to the band, like their age (year of birth), height, gender but also some goals - sleep goal, step goal and so on. Gadgetbridge contains a global place for user data preferences. Class ActivityUser
is holding the common user information needed by most activity trackers. Users can set their preference in Gadgetbridge → Settings → About you
and you can retrieve this data by accessing the class methods, like for example activityUser.getHeightCm()
Some of these user data might also be dependent on their preference of units - metric or imperial. This is handled by a global settings Gadgetbridge → Settings → Language and region settings
, accesible via GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric))
.
Final Words
We hope that this tutorial will help you get started with adding support for new devices in Gadgetbridge.
For questions and improvements, do not hesitate to contact us!
Happy hacking, the Gadgetbridge Team
General
- Home
- FAQ
- ReadMe
- Configuration
- Notifications
- ChangeLog
- Widget
- Weather
- Data Backup
- Pairing
- Find phone
- Music info
- Permissions Explained
- Firmware Update
- Automation via Intents
Sports/Activities
- Sports Activities Workouts
- Activity Sessions List
- Activity and Sleep Charts
- Heartrate measurement
- Integrating Sports Tracking apps with Gadgetbridge Sports Activities/Workouts
Smart Device Related
- Bangle.js
- Casio devices
- FitPro
- Fossil Hybrid HR
- Garmin devices
- HPlus
- Huami devices
- Amazfit Band 5
- Amazfit Band 7
- Amazfit Bip
- Amazfit Bip Lite
- Amazfit Bip S
- Amazfit Bip U
- Amazfit Bip 3 Pro
- Amazfit Bip 5
- Amazfit Cheetah
- Amazfit Cheetah Pro
- Amazfit Cor
- Amazfit Cor 2
- Amazfit Falcon
- Amazfit GTR
- Amazfit GTR 3
- Amazfit GTR 3 Pro
- Amazfit GTR 4
- Amazfit GTR Mini
- Amazfit GTS
- Amazfit GTS 3
- Amazfit GTS 4
- Amazfit GTS 4 Mini
- Amazfit Neo
- Amazfit T-Rex
- Amazfit T-Rex 2
- Amazfit T-Rex Ultra
- Mi Band 1
- Mi Band 2
- Mi Band 3
- Mi Band 4
- Mi Band 5
- Mi Band 6
- Mi Band 7
- MyKronoz ZeTime
- Pebble
- PineTime
- Sony Wena 3
- SMA
Wireless Earbuds
Others
- iTag Keyring trackers
- Nut Keyring trackers
- UM25 USB Voltage meter
- VESC BLDC controller VESC
- Flipper Zero Multi-tool Device for Geeks
- Roidmi Roidmi/Mojietu FM Trans.
- Vibratissimo Private toy
- Shell Racing Toy RC cars
Full list of supported devices
Development
- How to Release
- Developer Documentation
- BT Protocol Reverse Engineering
- Support for a new Device
- New Device Tutorial
- Translating Gadgetbridge
- OpenTracks-API
- Intent-API
Feature Discussion
FAQ