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:
parent
46dd45cb4e
commit
d53971c881
@ -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
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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),
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user