mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Sony Headphones: Switch to BTBR support class
This commit is contained in:
-163
@@ -1,163 +0,0 @@
|
||||
/* Copyright (C) 2021-2024 Daniel Dakhno, José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Message;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class SonyHeadphonesIoThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesIoThread.class);
|
||||
|
||||
private final SonyHeadphonesProtocol mProtocol;
|
||||
|
||||
private final UUID btrfcommUuidV1 = UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA");
|
||||
private final UUID btrfcommUuidV2 = UUID.fromString("956C7B26-D49A-4BA8-B03F-B17D393CB6E2");
|
||||
|
||||
// Track whether we got the first init reply
|
||||
private final Handler handler = new Handler();
|
||||
private int initRetries = 0;
|
||||
|
||||
/**
|
||||
* Sometimes the headphones will ignore the first init request, so we retry a few times
|
||||
* TODO: Implement this in a more elegant way. Ideally, we should retry every command for which we didn't get an ACK.
|
||||
*/
|
||||
private final Runnable initSendRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// If we still haven't got any reply, re-send the init
|
||||
if (!mProtocol.hasProtocolImplementation()) {
|
||||
if (initRetries++ < 2) {
|
||||
LOG.warn("Init retry {}", initRetries);
|
||||
|
||||
mProtocol.decreasePendingAcks();
|
||||
write(mProtocol.encodeInit());
|
||||
scheduleInitRetry();
|
||||
} else {
|
||||
LOG.error("Failed to start headphones init after {} tries", initRetries);
|
||||
quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public SonyHeadphonesIoThread(final GBDevice gbDevice,
|
||||
final Context context,
|
||||
final SonyHeadphonesProtocol protocol,
|
||||
final SonyHeadphonesSupport support,
|
||||
final BluetoothAdapter btAdapter) {
|
||||
super(gbDevice, context, protocol, support, btAdapter);
|
||||
mProtocol = protocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
write(mProtocol.encodeInit());
|
||||
scheduleInitRetry();
|
||||
setUpdateState(GBDevice.State.INITIALIZING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quit() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
super.quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(final byte[] bytes) {
|
||||
// Log the human-readable message, for debugging
|
||||
LOG.info("Writing {}", Message.fromBytes(bytes));
|
||||
|
||||
super.write(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] parseIncoming(final InputStream inputStream) throws IOException {
|
||||
final ByteArrayOutputStream msgStream = new ByteArrayOutputStream();
|
||||
final byte[] incoming = new byte[1];
|
||||
|
||||
do {
|
||||
inputStream.read(incoming);
|
||||
|
||||
if (incoming[0] == Message.MESSAGE_HEADER) {
|
||||
msgStream.reset();
|
||||
}
|
||||
|
||||
msgStream.write(incoming);
|
||||
} while (incoming[0] != Message.MESSAGE_TRAILER);
|
||||
|
||||
LOG.trace("Raw message: {}", GB.hexdump(msgStream.toByteArray()));
|
||||
|
||||
return msgStream.toByteArray();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) {
|
||||
boolean hasV1 = false;
|
||||
boolean hasV2 = false;
|
||||
boolean preferV2 = getCoordinator().preferServiceV2();
|
||||
for (final ParcelUuid uuid : uuids) {
|
||||
if (uuid.getUuid().equals(btrfcommUuidV1)) {
|
||||
LOG.info("Found Sony UUID V1");
|
||||
hasV1 = true;
|
||||
} else if (uuid.getUuid().equals(btrfcommUuidV2)) {
|
||||
LOG.info("Found Sony UUID V2");
|
||||
hasV2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasV2) {
|
||||
LOG.info("Using Sony UUID V2");
|
||||
return btrfcommUuidV2;
|
||||
} else if (hasV1) {
|
||||
LOG.info("Using Sony UUID V1");
|
||||
return btrfcommUuidV1;
|
||||
}
|
||||
|
||||
LOG.warn("Failed to find a known Sony UUID, will fallback to {}", (preferV2 ? "V2" : "V1"));
|
||||
|
||||
return preferV2 ? btrfcommUuidV2 : btrfcommUuidV1;
|
||||
}
|
||||
|
||||
private void scheduleInitRetry() {
|
||||
LOG.info("Scheduling init retry");
|
||||
|
||||
handler.postDelayed(initSendRunnable, 1250);
|
||||
}
|
||||
|
||||
private SonyHeadphonesCoordinator getCoordinator() {
|
||||
return (SonyHeadphonesCoordinator) getDevice().getDeviceCoordinator();
|
||||
}
|
||||
}
|
||||
+2
@@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.prot
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.SonyProtocolImplV1;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2.SonyProtocolImplV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class SonyHeadphonesProtocol extends GBDeviceProtocol {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesProtocol.class);
|
||||
@@ -78,6 +79,7 @@ public class SonyHeadphonesProtocol extends GBDeviceProtocol {
|
||||
public GBDeviceEvent[] decodeResponse(byte[] res) {
|
||||
final Message message = Message.fromBytes(res);
|
||||
if (message == null) {
|
||||
LOG.warn("Failed to decode message from {}", GB.hexdump(res));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+160
-25
@@ -16,41 +16,83 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.os.Handler;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneSerialDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.deviceevents.SonyHeadphonesEnqueueRequestEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Message;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractHeadphoneSerialDeviceSupportV2;
|
||||
|
||||
public class SonyHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupportV2<SonyHeadphonesProtocol> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesSupport.class);
|
||||
|
||||
private static final UUID BTRFCOMM_UUID_V1 = UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA");
|
||||
private static final UUID BTRFCOMM_UUID_V2 = UUID.fromString("956C7B26-D49A-4BA8-B03F-B17D393CB6E2");
|
||||
|
||||
// Track whether we got the first init reply
|
||||
private final Handler handler = new Handler();
|
||||
private int initRetries = 0;
|
||||
|
||||
private final ByteBuffer packetBuffer = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/**
|
||||
* Sometimes the headphones will ignore the first init request, so we retry a few times
|
||||
* TODO: Implement this in a more elegant way. Ideally, we should retry every command for which we didn't get an ACK.
|
||||
*/
|
||||
private final Runnable initSendRunnable = () -> {
|
||||
// If we still haven't got any reply, re-send the init
|
||||
if (!mDeviceProtocol.hasProtocolImplementation()) {
|
||||
if (initRetries++ < 2) {
|
||||
LOG.warn("Init retry {}", initRetries);
|
||||
|
||||
mDeviceProtocol.decreasePendingAcks();
|
||||
final TransactionBuilder builder = createTransactionBuilder("init retry " + initRetries);
|
||||
builder.write(mDeviceProtocol.encodeInit());
|
||||
builder.queue();
|
||||
scheduleInitRetry();
|
||||
} else {
|
||||
LOG.error("Failed to start headphones init after {} tries", initRetries);
|
||||
if (GBApplication.getPrefs().getAutoReconnect(getDevice())) {
|
||||
LOG.debug("will wait for reconnect");
|
||||
gbDevice.setUpdateState(GBDevice.State.WAITING_FOR_RECONNECT, getContext());
|
||||
} else {
|
||||
LOG.debug("disconnecting");
|
||||
gbDevice.setUpdateState(GBDevice.State.NOT_CONNECTED, getContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public class SonyHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupport {
|
||||
@Override
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
protected SonyHeadphonesProtocol createDeviceProtocol() {
|
||||
return new SonyHeadphonesProtocol(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new SonyHeadphonesIoThread(getDevice(), getContext(), (SonyHeadphonesProtocol) getDeviceProtocol(), SonyHeadphonesSupport.this, getBluetoothAdapter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SonyHeadphonesIoThread getDeviceIOThread() {
|
||||
return (SonyHeadphonesIoThread) super.getDeviceIOThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) {
|
||||
final SonyHeadphonesProtocol sonyProtocol = (SonyHeadphonesProtocol) getDeviceProtocol();
|
||||
if (deviceEvent instanceof SonyHeadphonesEnqueueRequestEvent enqueueRequestEvent) {
|
||||
mDeviceProtocol.enqueueRequests(enqueueRequestEvent.getRequests());
|
||||
|
||||
if (deviceEvent instanceof SonyHeadphonesEnqueueRequestEvent) {
|
||||
final SonyHeadphonesEnqueueRequestEvent enqueueRequestEvent = (SonyHeadphonesEnqueueRequestEvent) deviceEvent;
|
||||
sonyProtocol.enqueueRequests(enqueueRequestEvent.getRequests());
|
||||
|
||||
if (sonyProtocol.getPendingAcks() == 0) {
|
||||
if (mDeviceProtocol.getPendingAcks() == 0) {
|
||||
// There are no pending acks, send one request from the queue
|
||||
// TODO: A more elegant way of scheduling these?
|
||||
SonyHeadphonesIoThread deviceIOThread = getDeviceIOThread();
|
||||
deviceIOThread.write(sonyProtocol.getFromQueue());
|
||||
final TransactionBuilder builder = createTransactionBuilder("enqueue request");
|
||||
builder.write(mDeviceProtocol.getFromQueue());
|
||||
builder.queue();
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -60,7 +102,100 @@ public class SonyHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupport
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
|
||||
packetBuffer.clear();
|
||||
builder.write(mDeviceProtocol.encodeInit());
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZING);
|
||||
scheduleInitRetry();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
synchronized (ConnectionMonitor) {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSocketRead(final byte[] data) {
|
||||
packetBuffer.put(data);
|
||||
packetBuffer.flip();
|
||||
|
||||
while (packetBuffer.hasRemaining()) {
|
||||
final int start = packetBuffer.position();
|
||||
packetBuffer.mark();
|
||||
|
||||
final byte header = packetBuffer.get();
|
||||
if (header != Message.MESSAGE_HEADER) {
|
||||
LOG.warn("Unexpected header byte: {}", String.format("0x%02x", header));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find trailer after header
|
||||
int end = -1;
|
||||
for (int i = start + 1; i < packetBuffer.limit(); i++) {
|
||||
if (packetBuffer.get(i) == Message.MESSAGE_TRAILER) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (end < 0) {
|
||||
// Header found but trailer not yet available
|
||||
packetBuffer.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
final byte[] messageBytes = new byte[end - start + 1];
|
||||
|
||||
packetBuffer.position(start);
|
||||
packetBuffer.get(messageBytes);
|
||||
|
||||
// Hand it upstream, to the protocol
|
||||
try {
|
||||
super.onSocketRead(messageBytes);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error handling message", e);
|
||||
}
|
||||
}
|
||||
|
||||
packetBuffer.compact();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getSupportedService() {
|
||||
boolean hasV1 = false;
|
||||
boolean hasV2 = false;
|
||||
boolean preferV2 = ((SonyHeadphonesCoordinator) getDevice().getDeviceCoordinator()).preferServiceV2();
|
||||
final ParcelUuid[] uuids = getBluetoothDeviceUuids();
|
||||
for (final ParcelUuid uuid : uuids) {
|
||||
if (uuid.getUuid().equals(BTRFCOMM_UUID_V1)) {
|
||||
LOG.info("Found Sony UUID V1");
|
||||
hasV1 = true;
|
||||
} else if (uuid.getUuid().equals(BTRFCOMM_UUID_V2)) {
|
||||
LOG.info("Found Sony UUID V2");
|
||||
hasV2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasV2) {
|
||||
LOG.info("Using Sony UUID V2");
|
||||
return BTRFCOMM_UUID_V2;
|
||||
} else if (hasV1) {
|
||||
LOG.info("Using Sony UUID V1");
|
||||
return BTRFCOMM_UUID_V1;
|
||||
}
|
||||
|
||||
LOG.warn("Failed to find a known Sony UUID, will fallback to {}", (preferV2 ? "V2" : "V1"));
|
||||
|
||||
return preferV2 ? BTRFCOMM_UUID_V2 : BTRFCOMM_UUID_V1;
|
||||
}
|
||||
|
||||
private void scheduleInitRetry() {
|
||||
LOG.info("Scheduling init retry");
|
||||
|
||||
handler.postDelayed(initSendRunnable, 1250);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user