1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-27 19:15:50 +01:00

Add initial Bowers and Wilkins P series support. (#4288)

Co-authored-by: mvn23 <schopdiedwaas@gmail.com>
Co-committed-by: mvn23 <schopdiedwaas@gmail.com>
This commit is contained in:
mvn23 2024-11-01 21:06:34 +00:00 committed by José Rebelo
parent 46dd45cb4e
commit d53971c881
9 changed files with 439 additions and 0 deletions

View File

@ -246,6 +246,7 @@ dependencies {
implementation 'com.github.wax911.android-emojify:gson:1.9.4'
implementation 'com.google.protobuf:protobuf-javalite:4.28.2'
implementation 'com.android.volley:volley:1.2.1'
implementation 'org.msgpack:msgpack-core:0.9.8'
// Bouncy Castle is included directly in GB, to avoid pulling the entire dependency
// It's included in the org.bouncycastle.shaded package, to fix conflicts with roboelectric

View File

@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries.BandWPSeriesDeviceSupport;
public class BandWPSeriesDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_bandw_pseries;
}
@Override
public String getManufacturer() {
return "Bowers and Wilkins";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return BandWPSeriesDeviceSupport.class;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("LE_BWHP");
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
}
@Override
public int getBatteryCount() {
return 3;
}
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
BatteryConfig battery0 = new BatteryConfig(0, R.drawable.ic_earbuds_battery, R.string.left_earbud);
BatteryConfig battery1 = new BatteryConfig(1, R.drawable.ic_earbuds_battery, R.string.right_earbud);
BatteryConfig battery2 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case);
return new BatteryConfig[]{battery0, battery1, battery2};
}
}

View File

@ -30,6 +30,7 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.asteroidos.AsteroidOSDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries.BandWPSeriesDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.binary_sensor.coordinator.BinarySensorCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
@ -543,6 +544,7 @@ public enum DeviceType {
COLMI_R03(ColmiR03Coordinator.class),
COLMI_R06(ColmiR06Coordinator.class),
COLMI_R10(ColmiR10Coordinator.class),
B_AND_W_P_SERIES(BandWPSeriesDeviceCoordinator.class),
SCANNABLE(ScannableDeviceCoordinator.class),
CYCLING_SENSOR(CyclingSensorCoordinator.class),
BLE_GATT_CLIENT(BleGattClientCoordinator.class),

View File

@ -0,0 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
private static final Logger LOG = LoggerFactory.getLogger(BandWBLEProfile.class);
private static final String ACTION_PREFIX = BandWBLEProfile.class.getName() + "_";
public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO";
public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO";
public static final UUID UUID_RPC_REQUEST_CHARACTERISTIC = UUID.fromString("ada50ce9-67b8-4a97-9d8e-37e1d083156c");
public BandWBLEProfile(final T support) {
super(support);
}
public void requestDeviceName(final TransactionBuilder builder) {
sendRequest(builder, (byte) 0x05, (byte) 0x01);
}
public void requestFirmware(final TransactionBuilder builder) {
sendRequest(builder, (byte) 0x02, (byte) 0x01);
}
public void requestBatteryLevels(final TransactionBuilder builder) {
sendRequest(builder, (byte) 0x08, (byte) 0x17);
}
private void sendRequest(final TransactionBuilder builder, byte namespace, byte commandID) {
BandWPSeriesRequest req;
try {
req = new BandWPSeriesRequest(namespace, commandID);
} catch (IOException e) {
LOG.error("Failed to send request: namespace {}, commandID {}", namespace, commandID);
return;
}
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
}
}

View File

@ -0,0 +1,27 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
public enum BandWMessageType {
REQUEST_WITH_PAYLOAD(0x920b, true),
REQUEST_WITHOUT_PAYLOAD(0x120b, false),
RESPONSE_WITH_PAYLOAD(0x920c, true),
RESPONSE_WITHOUT_PAYLOAD(0x120c, false),
NOTIFICATION_WITH_PAYLOAD(0x920d, true),
NOTIFICATION_WITHOUT_PAYLOAD(0x120d, false);
public final int value;
public final boolean hasPayload;
BandWMessageType(int mType, boolean hasPayload) {
this.value = mType;
this.hasPayload = hasPayload;
}
public static BandWMessageType getByType(int mType) {
for (BandWMessageType t: values()) {
if (t.value == mType) {
return t;
}
}
return null;
}
}

View File

@ -0,0 +1,153 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
import static nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.BATTERY_UNKNOWN;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGatt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16Constants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesDeviceSupport.class);
private static final UUID UUID_RPC_SERVICE = UUID.fromString("85ba93a5-09ac-439a-8cc4-1c3f0cb4f29f");
private static final UUID UUID_RPC_RESPONSE_CHARACTERISTIC = UUID.fromString("cb909093-3559-4b0c-9a7f-3f1773122fdc");
private final BandWBLEProfile<BandWPSeriesDeviceSupport> BandWBLEProfile;
private final GBDeviceEventBatteryInfo[] batteryInfo = new GBDeviceEventBatteryInfo[3];
public BandWPSeriesDeviceSupport() {
super(LOG);
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE);
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE);
addSupportedService(UUID_RPC_SERVICE);
BandWBLEProfile = new BandWBLEProfile<>(this);
addSupportedProfile(BandWBLEProfile);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
// mark the device as initializing
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
getDevice().setBatteryLabel(R.string.left_earbud, 0);
getDevice().setBatteryLabel(R.string.right_earbud, 1);
getDevice().setBatteryLabel(R.string.battery_case, 2);
for (int i = 0; i < 3; i++) {
batteryInfo[i] = new GBDeviceEventBatteryInfo();
batteryInfo[i].batteryIndex = i;
batteryInfo[i].level = BATTERY_UNKNOWN;
handleGBDeviceEvent(batteryInfo[i]);
}
// mark the device as initialized
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
builder.notify(getCharacteristic(UUID_RPC_RESPONSE_CHARACTERISTIC), true);
BandWBLEProfile.requestFirmware(builder);
BandWBLEProfile.requestDeviceName(builder);
BandWBLEProfile.requestBatteryLevels(builder);
return builder;
}
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
UUID characteristicUUID = characteristic.getUuid();
if (UUID_RPC_RESPONSE_CHARACTERISTIC.equals(characteristicUUID)) {
return handleRPCResponse(characteristic);
}
return false;
}
private boolean handleRPCResponse(BluetoothGattCharacteristic characteristic) {
BandWPSeriesResponse response = new BandWPSeriesResponse(characteristic.getValue());
LOG.debug("Got RPC response: Type {}, commandID {}, namespace {}, errorCode {}, payload {}",
response.messageType,
response.commandId,
response.namespace,
response.errorCode,
response.payload);
if (response.errorCode != 0) {
return false;
}
if (response.namespace == 0x02) {
if (response.commandId == 0x01) {
return handleFirmwareVersionResponse(response);
}
} else if (response.namespace == 0x05) {
if (response.commandId == 0x01) {
return handleDeviceNameResponse(response);
}
} else if (response.namespace == 0x08) {
if (response.commandId == 0x17) {
return handleBatteryLevels(response);
}
}
return true;
}
private boolean handleBatteryLevels(BandWPSeriesResponse response) {
int[] levels = response.getPayloadFixArray();
if (levels == null) {
return false;
}
for (int i = 0; i < levels.length; i++) {
if (i >= 3) {
break;
}
int level = (levels[i] == 0xff) ? BATTERY_UNKNOWN : levels[i];
LOG.debug("Battery {} has level {}", i, levels[i]);
batteryInfo[i].level = level;
handleGBDeviceEvent(batteryInfo[i]);
}
return true;
}
private boolean handleFirmwareVersionResponse(BandWPSeriesResponse response) {
String firmwareString = response.getPayloadString();
if (firmwareString == null) {
return false;
}
String[] versions = firmwareString.split("\\(");
String main_version = versions[0];
String sub_version = versions[1].substring(0, versions[1].length()-1);
GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
versionInfo.fwVersion = main_version;
versionInfo.fwVersion2 = sub_version;
LOG.debug("Got firmware version {}/{}", main_version, sub_version);
handleGBDeviceEvent(versionInfo);
return true;
}
private boolean handleDeviceNameResponse(BandWPSeriesResponse response) {
String deviceName = response.getPayloadString();
if (deviceName == null) {
return false;
}
getDevice().setName(deviceName);
LOG.debug("Set device name to {}", deviceName);
return true;
}
@Override
public boolean useAutoConnect() {
return true;
}
}

View File

@ -0,0 +1,70 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
import org.msgpack.core.MessageBufferPacker;
import org.msgpack.core.MessagePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class BandWPSeriesRequest {
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesRequest.class);
BandWMessageType messageType;
final byte namespace;
final byte commandId;
private final MessageBufferPacker payloadPacker = MessagePack.newDefaultBufferPacker();
public BandWPSeriesRequest(byte mNamespace, byte mCommandId) throws IOException {
messageType = BandWMessageType.REQUEST_WITHOUT_PAYLOAD;
namespace = mNamespace;
commandId = mCommandId;
payloadPacker.packInt(0);
}
public BandWPSeriesRequest addToPayload(int value) throws IOException {
payloadPacker.packInt(value);
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
return this;
}
public BandWPSeriesRequest addToPayload(byte value) throws IOException {
payloadPacker.packByte(value);
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
return this;
}
public BandWPSeriesRequest addToPayload(String value) throws IOException {
payloadPacker.packString(value);
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
return this;
}
public byte[] finishAndGetBytes() {
byte len = (byte) ((this.messageType == BandWMessageType.REQUEST_WITHOUT_PAYLOAD) ? 4 : 4 + payloadPacker.getBufferSize());
byte[] out = addMessageType(new byte[len+1], messageType.value);
out[0] = len;
out[3] = commandId;
out[4] = namespace;
if (messageType == BandWMessageType.REQUEST_WITH_PAYLOAD) {
System.arraycopy(payloadPacker.toByteArray(), 0, out, 5, len - 5);
}
try {
payloadPacker.close();
} catch (IOException e) {
LOG.warn("Failed to close payloadPacker");
}
return out;
}
private byte[] addMessageType(byte[] target, int value) {
byte valueLo = (byte) (value & 0xff);
byte valueHi = (byte) (value >> 8);
target[1] = valueLo;
target[2] = valueHi;
return target;
}
}

View File

@ -0,0 +1,80 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
import org.bouncycastle.shaded.util.Arrays;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class BandWPSeriesResponse {
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesResponse.class);
BandWMessageType messageType;
final byte namespace;
final byte commandId;
final int errorCode;
final int payloadLength;
final byte[] payload;
public final MessageUnpacker payloadUnpacker;
BandWPSeriesResponse(byte[] contents) {
messageType = BandWMessageType.getByType(getUInt16(Arrays.copyOfRange(contents, 0, 2)));
commandId = contents[2];
namespace = contents[3];
int payloadOffset = 6;
if (messageType == BandWMessageType.RESPONSE_WITH_PAYLOAD || messageType == BandWMessageType.RESPONSE_WITHOUT_PAYLOAD) {
errorCode = getUInt16(Arrays.copyOfRange(contents, 4, 6));
} else {
errorCode = 0;
payloadOffset = 4;
}
if (messageType == null || !messageType.hasPayload || errorCode != 0) {
payloadLength = 0;
payload = null;
payloadUnpacker = null;
} else {
payloadLength = getUInt16(Arrays.copyOfRange(contents, payloadOffset, payloadOffset + 2));
payload = Arrays.copyOfRange(contents, payloadOffset + 2, contents.length);
payloadUnpacker = MessagePack.newDefaultUnpacker(payload);
}
}
private int getUInt16(byte[] buffer) {
return (0xff & buffer[0]) | ((0xff & buffer[1]) << 8);
}
public String getPayloadString() {
String value;
try {
value = payloadUnpacker.unpackString();
} catch (IOException e) {
LOG.warn("Failed to unpack String from payload {}", payload);
return null;
}
return value;
}
public int[] getPayloadFixArray() {
int length;
try {
length = payloadUnpacker.unpackArrayHeader();
} catch (IOException e) {
LOG.warn("Failed to unpack ArrayHeader from payload {}", payload);
return null;
}
int[] values = new int[length];
try {
for (int i = 0; i < length; i++) {
values[i] = payloadUnpacker.unpackInt();
}
} catch (IOException e) {
LOG.warn("Failed to unpack byte from fixarray in payload {}", payload);
return null;
}
return values;
}
}

View File

@ -1875,6 +1875,7 @@
<string name="devicetype_colmi_r03">Colmi R03</string>
<string name="devicetype_colmi_r06">Colmi R06</string>
<string name="devicetype_colmi_r10">Colmi R10</string>
<string name="devicetype_bandw_pseries">Bowers and Wilkins P series</string>
<string name="choose_auto_export_location">Choose export location</string>
<string name="notification_channel_name">General</string>
<string name="notification_channel_high_priority_name">High-priority</string>