mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Nothing Ear: switch to AbstractHeadphoneBTBRDeviceSupport support class
Refactor Ear1Support to extend AbstractHeadphoneBTBRDeviceSupport instead of AbstractHeadphoneSerialDeviceSupport. Encapsulate NothingProtocol as inner class to Ear1Support and remove NothingIOThread. Rename one preference to allow the usage of Enum for audio mode. Beside of this change most of NothingProtocol remained unchanged. Also remove (unused) import in PixooIOThread.
This commit is contained in:
committed by
José Rebelo
parent
3aac1d5465
commit
7205318131
-1
@@ -32,7 +32,6 @@ import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.nothing.NothingProtocol;
|
||||
|
||||
public class PixooIOThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PixooIOThread.class);
|
||||
|
||||
+376
-17
@@ -16,19 +16,45 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneSerialDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Ear1Support extends AbstractHeadphoneSerialDeviceSupport {
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.AbstractEarCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneBTBRDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||
|
||||
public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Ear1Support.class);
|
||||
private static final int MAX_MTU = 512;
|
||||
private static final UUID UUID_DEVICE_CTRL = UUID.fromString("aeac4a03-dff5-498f-843a-34487cf133eb");
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
super.onSendConfiguration(config);
|
||||
private NothingProtocol nothingProtocol;
|
||||
|
||||
public Ear1Support() {
|
||||
super(LOG, MAX_MTU);
|
||||
addSupportedService(UUID_DEVICE_CTRL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,24 +62,357 @@ public class Ear1Support extends AbstractHeadphoneSerialDeviceSupport {
|
||||
//getDeviceIOThread().write(((NothingProtocol) getDeviceProtocol()).encodeBatteryStatusReq());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized NothingIOThread getDeviceIOThread() {
|
||||
return (NothingIOThread) super.getDeviceIOThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
return new NothingProtocol(getDevice());
|
||||
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
|
||||
nothingProtocol = new NothingProtocol(getCoordinator().incrementCounter());
|
||||
|
||||
sendCommand(builder, nothingProtocol.encodeBatteryStatusReq());
|
||||
sendCommand(builder, nothingProtocol.encodeAudioModeStatusReq());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private AbstractEarCoordinator getCoordinator() {
|
||||
return (AbstractEarCoordinator) getDevice().getDeviceCoordinator();
|
||||
}
|
||||
|
||||
private void sendCommand(final TransactionBuilder builder, final byte[] payload) {
|
||||
builder.write(payload);
|
||||
}
|
||||
|
||||
private void sendCommand(final String taskName, final byte[] payload) {
|
||||
final TransactionBuilder builder = createTransactionBuilder(taskName);
|
||||
sendCommand(builder, payload);
|
||||
builder.queue();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSocketRead(byte[] data) {
|
||||
for (GBDeviceEvent deviceEvent : nothingProtocol.decodeResponse(data)) {
|
||||
if (deviceEvent == null) {
|
||||
continue;
|
||||
}
|
||||
evaluateGBDeviceEvent(deviceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new NothingIOThread(getDevice(), getContext(), (NothingProtocol) getDeviceProtocol(),
|
||||
Ear1Support.this, getBluetoothAdapter());
|
||||
public void onFindDevice(boolean start) {
|
||||
sendCommand("find device", nothingProtocol.encodeFindDevice(start));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
sendCommand("set time", nothingProtocol.encodeSetTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(final String config) {
|
||||
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR:
|
||||
byte enabled = (byte) (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR, true) ? 0x01 : 0x00);
|
||||
sendCommand("set in ear detection", nothingProtocol.encodeInEarDetection(enabled));
|
||||
// response: 55 20 01 04 70 00 00 00
|
||||
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE:
|
||||
sendCommand("set audio mode", nothingProtocol.encodeAudioMode(prefs.getString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off")));
|
||||
// response: 55 20 01 0F 70 00 00 00
|
||||
default:
|
||||
LOG.debug("CONFIG: " + config);
|
||||
}
|
||||
|
||||
super.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
static class NothingProtocol {
|
||||
|
||||
private boolean isFirstExchange = true;
|
||||
|
||||
private static final byte CONTROL_DEVICE_TYPE_TWS_HEADSET = 1;
|
||||
|
||||
private static final int CONTROL_CRC = 0x20;
|
||||
|
||||
private static final byte MASK_RSP_CODE = 0x1f;
|
||||
private static final short MASK_DEVICE_TYPE = 0x0F00;
|
||||
|
||||
private static final short MASK_REQUEST_CMD = (short) 0x8000;
|
||||
|
||||
private static final byte MASK_BATTERY = 0x7f;
|
||||
private static final byte MASK_BATTERY_CHARGING = (byte) 0x80;
|
||||
|
||||
//incoming
|
||||
private static final short battery_status = (short) 0xe001;
|
||||
private static final short battery_status2 = (short) 0xc007;
|
||||
private static final short audio_mode_status = (short) 0xc01e;
|
||||
|
||||
private static final short unk_maybe_ack = (short) 0xf002;
|
||||
private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
|
||||
|
||||
//outgoing
|
||||
private static final short find_device = (short) 0xf002;
|
||||
private static final short in_ear_detection = (short) 0xf004;
|
||||
private static final short audio_mode = (short) 0xf00f;
|
||||
|
||||
private final boolean incrementCounter;
|
||||
private int messageCounter = 0x00;
|
||||
|
||||
private final HashMap<Byte, GBDeviceEventBatteryInfo> batteries;
|
||||
private static final byte battery_earphone_left = 0x02;
|
||||
private static final byte battery_earphone_right = 0x03;
|
||||
private static final byte battery_case = 0x04;
|
||||
|
||||
private enum NothingAudioMode {
|
||||
anc(0x01),
|
||||
anclight(0x03),
|
||||
off(0x05),
|
||||
transparency(0x07),
|
||||
;
|
||||
|
||||
private final int bitmask;
|
||||
|
||||
NothingAudioMode (int bitmask) {
|
||||
this.bitmask = bitmask;
|
||||
}
|
||||
|
||||
public int getBitmask() { return bitmask; }
|
||||
|
||||
public static NothingAudioMode fromBitmask(int bitmask) {
|
||||
for (NothingAudioMode flag : values()) {
|
||||
if (flag.bitmask == bitmask) {
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown NothingAudioMode: 0x" +
|
||||
Integer.toHexString(bitmask));
|
||||
}
|
||||
}
|
||||
|
||||
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||
List<GBDeviceEvent> devEvts = new ArrayList<>();
|
||||
|
||||
if (isFirstExchange) {
|
||||
isFirstExchange = false;
|
||||
devEvts.add(new GBDeviceEventVersionInfo()); //TODO: this is a weird hack to make the DBHelper happy. Replace with proper firmware detection
|
||||
devEvts.add(new GBDeviceEventUpdateDeviceState(GBDevice.State.INITIALIZED));
|
||||
|
||||
}
|
||||
|
||||
ByteBuffer incoming = ByteBuffer.wrap(responseData);
|
||||
incoming.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
byte sof = incoming.get();
|
||||
if (sof != 0x55) {
|
||||
LOG.error("Error in message, wrong start of frame: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
|
||||
short control = incoming.getShort();
|
||||
if (!isSupportedDevice(control)) {
|
||||
LOG.error("Unsupported device specified in message: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
if (!isOk(control)) {
|
||||
LOG.error("Message is not ok: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
|
||||
short command = incoming.getShort();
|
||||
short length = incoming.getShort();
|
||||
incoming.get();
|
||||
|
||||
byte[] payload = Arrays.copyOfRange(responseData, incoming.position(), incoming.position() + length);
|
||||
|
||||
|
||||
switch (getRequestCommand(command)) {
|
||||
case battery_status:
|
||||
case battery_status2:
|
||||
devEvts.addAll(handleBatteryInfo(payload));
|
||||
break;
|
||||
case audio_mode_status:
|
||||
devEvts.add(handleAudioModeStatus(payload));
|
||||
break;
|
||||
|
||||
case unk_maybe_ack:
|
||||
LOG.debug("received ack");
|
||||
break;
|
||||
case unk_close_case:
|
||||
LOG.debug("case closed");
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG.debug("Incoming message - control:" + control + " requestCommand: " + (getRequestCommand(command) & 0xffff) + "length: " + length + " dump: " + hexdump(responseData));
|
||||
|
||||
}
|
||||
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
|
||||
}
|
||||
|
||||
boolean isCrcNeeded(short control) {
|
||||
return (control & CONTROL_CRC) != 0;
|
||||
}
|
||||
|
||||
byte[] encodeMessage(short control, short command, byte[] payload) {
|
||||
|
||||
ByteBuffer msgBuf = ByteBuffer.allocate(8 + payload.length);
|
||||
msgBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
msgBuf.put((byte) 0x55); //sof
|
||||
msgBuf.putShort((short) (incrementCounter ? (control | 0x40) : control));
|
||||
msgBuf.putShort(command);
|
||||
msgBuf.putShort((short) payload.length);
|
||||
msgBuf.put((byte) messageCounter); //fsn
|
||||
if (incrementCounter) {
|
||||
messageCounter++;
|
||||
if ((byte) messageCounter == (byte) 0xfd) {
|
||||
messageCounter = 0x00;
|
||||
}
|
||||
}
|
||||
msgBuf.put(payload);
|
||||
|
||||
if (isCrcNeeded(control)) {
|
||||
msgBuf.position(0);
|
||||
ByteBuffer crcBuf = ByteBuffer.allocate(msgBuf.capacity() + 2);
|
||||
crcBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
crcBuf.put(msgBuf);
|
||||
crcBuf.putShort((short) getCRC16ansi(msgBuf.array()));
|
||||
return crcBuf.array();
|
||||
}
|
||||
|
||||
return msgBuf.array();
|
||||
}
|
||||
|
||||
byte[] encodeBatteryStatusReq() {
|
||||
return encodeMessage((short) 0x5120, battery_status2, new byte[]{});
|
||||
}
|
||||
|
||||
byte[] encodeAudioModeStatusReq() {
|
||||
return encodeMessage((short) 0x120, audio_mode_status, new byte[]{});
|
||||
}
|
||||
|
||||
private GBDeviceEventUpdatePreferences handleAudioModeStatus(byte[] payload) {
|
||||
final GBDeviceEventUpdatePreferences preferencesEvent = new GBDeviceEventUpdatePreferences();
|
||||
|
||||
if (payload.length == 3 && payload[0] == 0x01 && payload[2] == 0x00) {
|
||||
try {
|
||||
NothingAudioMode mode = NothingAudioMode.fromBitmask(payload[1]);
|
||||
LOG.info("Audio mode: " + mode.name());
|
||||
preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, mode.name());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Unknown audio mode. Payload: " + hexdump(payload));
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Invalid audio mode payload format. Payload: " + hexdump(payload));
|
||||
}
|
||||
return preferencesEvent;
|
||||
}
|
||||
|
||||
byte[] encodeInEarDetection(byte enabled) {
|
||||
return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
|
||||
}
|
||||
|
||||
byte[] encodeAudioMode(String desired) {
|
||||
int modeBitmask = NothingAudioMode.valueOf("off").getBitmask();
|
||||
try {
|
||||
modeBitmask = NothingAudioMode.valueOf(desired).getBitmask();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Illegal audio mode requested: {} , using default", desired);
|
||||
}
|
||||
byte[] payload = new byte[]{0x01, (byte) modeBitmask, 0x00};
|
||||
|
||||
return encodeMessage((short) 0x120, audio_mode, payload);
|
||||
}
|
||||
|
||||
public byte[] encodeFindDevice(boolean start) {
|
||||
byte payload = (byte) (start ? 0x01 : 0x00);
|
||||
return encodeMessage((short) 0x120, find_device, new byte[]{payload});
|
||||
}
|
||||
|
||||
public byte[] encodeSetTime() {
|
||||
// This are earphones, there is no time to set here. However this method gets called soon
|
||||
// after connecting, hence we use it to perform some initializations.
|
||||
// TODO: Find a way to send more requests during the first connection
|
||||
return encodeAudioModeStatusReq();
|
||||
}
|
||||
|
||||
private List<GBDeviceEvent> handleBatteryInfo(byte[] payload) {
|
||||
List<GBDeviceEvent> batEvts = new ArrayList<>();
|
||||
|
||||
//LOG.debug("Battery payload: " + hexdump(payload));
|
||||
|
||||
/* payload:
|
||||
1st byte is number of batteries, then $number pairs follow:
|
||||
{idx, value}
|
||||
|
||||
idx is 0x02 for left ear, 0x03 for right ear, 0x04 for case
|
||||
value goes from 0-64 (equivalent of 0-100 in hexadecimal)
|
||||
|
||||
|
||||
Since Gadgetbridge supports only one battery, we use an average of the levels for the
|
||||
battery level.
|
||||
If one of the batteries is recharging, we consider the battery as recharging.
|
||||
*/
|
||||
|
||||
// GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo();
|
||||
// evBattery.level = 0;
|
||||
// boolean batteryCharging = false;
|
||||
|
||||
int numBatteries = payload[0];
|
||||
for (int i = 0; i < numBatteries; i++) {
|
||||
|
||||
batteries.get(payload[1 + 2 * i]).level = (payload[2 + 2 * i] & MASK_BATTERY);
|
||||
batteries.get(payload[1 + 2 * i]).state =
|
||||
((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING) ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
|
||||
|
||||
batEvts.add(batteries.get(payload[1 + 2 * i]));
|
||||
|
||||
// evBattery.level += (short) ((payload[2 + 2 * i] & MASK_BATTERY) / numBatteries);
|
||||
// if (!batteryCharging) {
|
||||
// batteryCharging = ((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING);
|
||||
// }
|
||||
// LOG.debug("single battery level: " + hexdump(payload, 2+2*i,1) +"-"+ ((payload[2+2*i] & 0xff))+":" + evBattery.level);
|
||||
}
|
||||
|
||||
// evBattery.state = BatteryState.UNKNOWN;
|
||||
// evBattery.state = batteryCharging ? BatteryState.BATTERY_CHARGING : evBattery.state;
|
||||
|
||||
// return evBattery;
|
||||
return batEvts;
|
||||
}
|
||||
|
||||
private short getRequestCommand(short command) {
|
||||
return (short) (command | MASK_REQUEST_CMD);
|
||||
}
|
||||
|
||||
private boolean isOk(short control) {
|
||||
return (control & MASK_RSP_CODE) == 0;
|
||||
}
|
||||
|
||||
private boolean isSupportedDevice(short control) {
|
||||
return getDeviceType(control) == CONTROL_DEVICE_TYPE_TWS_HEADSET;
|
||||
}
|
||||
|
||||
private byte getDeviceType(short control) {
|
||||
return (byte) ((control & MASK_DEVICE_TYPE) >> 8);
|
||||
}
|
||||
|
||||
protected NothingProtocol(boolean incrementCounter) {
|
||||
batteries = new HashMap<>(3);
|
||||
|
||||
batteries.put(battery_earphone_left, new GBDeviceEventBatteryInfo());
|
||||
batteries.put(battery_earphone_right, new GBDeviceEventBatteryInfo());
|
||||
batteries.put(battery_case, new GBDeviceEventBatteryInfo());
|
||||
|
||||
batteries.get(battery_case).batteryIndex=0;
|
||||
batteries.get(battery_earphone_left).batteryIndex=1;
|
||||
batteries.get(battery_earphone_right).batteryIndex=2;
|
||||
|
||||
this.incrementCounter = incrementCounter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/* Copyright (C) 2021-2024 Daniele Gobbetti, Petr Vaněk
|
||||
|
||||
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.nothing;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||
|
||||
public class NothingIOThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NothingIOThread.class);
|
||||
|
||||
private final NothingProtocol mNothingProtocol;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
|
||||
return mNothingProtocol.UUID_DEVICE_CTRL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
write(mNothingProtocol.encodeBatteryStatusReq());
|
||||
write(mNothingProtocol.encodeAudioModeStatusReq());
|
||||
setUpdateState(GBDevice.State.INITIALIZED);
|
||||
}
|
||||
|
||||
public NothingIOThread(GBDevice device, Context context, NothingProtocol deviceProtocol,
|
||||
Ear1Support ear1Support, BluetoothAdapter bluetoothAdapter) {
|
||||
super(device, context, deviceProtocol, ear1Support, bluetoothAdapter);
|
||||
mNothingProtocol = deviceProtocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] parseIncoming(InputStream inStream) throws IOException {
|
||||
byte[] buffer = new byte[1048576]; //HUGE read
|
||||
int bytes = inStream.read(buffer);
|
||||
LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
|
||||
return Arrays.copyOf(buffer, bytes);
|
||||
}
|
||||
|
||||
}
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
/* Copyright (C) 2021-2024 Daniele Gobbetti, 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.nothing;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.AbstractEarCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||
|
||||
public class NothingProtocol extends GBDeviceProtocol {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NothingProtocol.class);
|
||||
|
||||
final UUID UUID_DEVICE_CTRL = UUID.fromString("aeac4a03-dff5-498f-843a-34487cf133eb");
|
||||
|
||||
private boolean isFirstExchange = true;
|
||||
|
||||
private static final byte CONTROL_DEVICE_TYPE_TWS_HEADSET = 1;
|
||||
|
||||
private static final int CONTROL_CRC = 0x20;
|
||||
|
||||
private static final byte MASK_RSP_CODE = 0x1f;
|
||||
private static final short MASK_DEVICE_TYPE = 0x0F00;
|
||||
|
||||
|
||||
private static final short MASK_REQUEST_CMD = (short) 0x8000;
|
||||
|
||||
private static final byte MASK_BATTERY = 0x7f;
|
||||
private static final byte MASK_BATTERY_CHARGING = (byte) 0x80;
|
||||
|
||||
|
||||
//incoming
|
||||
private static final short battery_status = (short) 0xe001;
|
||||
private static final short battery_status2 = (short) 0xc007;
|
||||
private static final short audio_mode_status = (short) 0xc01e;
|
||||
|
||||
private static final short unk_maybe_ack = (short) 0xf002;
|
||||
private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
|
||||
|
||||
//outgoing
|
||||
private static final short find_device = (short) 0xf002;
|
||||
private static final short in_ear_detection = (short) 0xf004;
|
||||
private static final short audio_mode = (short) 0xf00f;
|
||||
|
||||
private final boolean incrementCounter;
|
||||
private int messageCounter = 0x00;
|
||||
|
||||
private HashMap<Byte, GBDeviceEventBatteryInfo> batteries;
|
||||
private static final byte battery_earphone_left = 0x02;
|
||||
private static final byte battery_earphone_right = 0x03;
|
||||
private static final byte battery_case = 0x04;
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||
List<GBDeviceEvent> devEvts = new ArrayList<>();
|
||||
|
||||
if (isFirstExchange) {
|
||||
isFirstExchange = false;
|
||||
devEvts.add(new GBDeviceEventVersionInfo()); //TODO: this is a weird hack to make the DBHelper happy. Replace with proper firmware detection
|
||||
}
|
||||
|
||||
ByteBuffer incoming = ByteBuffer.wrap(responseData);
|
||||
incoming.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
byte sof = incoming.get();
|
||||
if (sof != 0x55) {
|
||||
LOG.error("Error in message, wrong start of frame: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
|
||||
short control = incoming.getShort();
|
||||
if (!isSupportedDevice(control)) {
|
||||
LOG.error("Unsupported device specified in message: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
if (!isOk(control)) {
|
||||
LOG.error("Message is not ok: " + hexdump(responseData));
|
||||
return null;
|
||||
}
|
||||
short command = incoming.getShort();
|
||||
short length = incoming.getShort();
|
||||
incoming.get();
|
||||
|
||||
byte[] payload = Arrays.copyOfRange(responseData, incoming.position(), incoming.position() + length);
|
||||
|
||||
|
||||
switch (getRequestCommand(command)) {
|
||||
case battery_status:
|
||||
case battery_status2:
|
||||
devEvts.addAll(handleBatteryInfo(payload));
|
||||
break;
|
||||
case audio_mode_status:
|
||||
devEvts.add(handleAudioModeStatus(payload));
|
||||
break;
|
||||
|
||||
case unk_maybe_ack:
|
||||
LOG.debug("received ack");
|
||||
break;
|
||||
case unk_close_case:
|
||||
LOG.debug("case closed");
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG.debug("Incoming message - control:" + control + " requestCommand: " + (getRequestCommand(command) & 0xffff) + "length: " + length + " dump: " + hexdump(responseData));
|
||||
|
||||
}
|
||||
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
|
||||
}
|
||||
|
||||
boolean isCrcNeeded(short control) {
|
||||
return (control & CONTROL_CRC) != 0;
|
||||
}
|
||||
|
||||
byte[] encodeMessage(short control, short command, byte[] payload) {
|
||||
|
||||
ByteBuffer msgBuf = ByteBuffer.allocate(8 + payload.length);
|
||||
msgBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
msgBuf.put((byte) 0x55); //sof
|
||||
msgBuf.putShort((short) (incrementCounter ? (control | 0x40) : control));
|
||||
msgBuf.putShort(command);
|
||||
msgBuf.putShort((short) payload.length);
|
||||
msgBuf.put((byte) messageCounter); //fsn
|
||||
if (incrementCounter) {
|
||||
messageCounter++;
|
||||
if ((byte) messageCounter == (byte) 0xfd) {
|
||||
messageCounter = 0x00;
|
||||
}
|
||||
}
|
||||
msgBuf.put(payload);
|
||||
|
||||
if (isCrcNeeded(control)) {
|
||||
msgBuf.position(0);
|
||||
ByteBuffer crcBuf = ByteBuffer.allocate(msgBuf.capacity() + 2);
|
||||
crcBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
crcBuf.put(msgBuf);
|
||||
crcBuf.putShort((short) getCRC16ansi(msgBuf.array()));
|
||||
return crcBuf.array();
|
||||
}
|
||||
|
||||
return msgBuf.array();
|
||||
}
|
||||
|
||||
byte[] encodeBatteryStatusReq() {
|
||||
return encodeMessage((short) 0x5120, battery_status2, new byte[]{});
|
||||
}
|
||||
|
||||
byte[] encodeAudioModeStatusReq() {
|
||||
return encodeMessage((short) 0x120, audio_mode_status, new byte[]{});
|
||||
}
|
||||
|
||||
//TODO: unify mapping between bytes and strings in the following two functions
|
||||
private GBDeviceEvent handleAudioModeStatus(byte[] payload) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
if (Arrays.equals(payload, new byte[]{0x01, 0x01, 0x00})) {
|
||||
editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "anc").apply();
|
||||
} else if (Arrays.equals(payload, new byte[]{0x01, 0x03, 0x00})) {
|
||||
editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "anc-light").apply();
|
||||
} else if (Arrays.equals(payload, new byte[]{0x01, 0x05, 0x00})) {
|
||||
editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off").apply();
|
||||
} else if (Arrays.equals(payload, new byte[]{0x01, 0x07, 0x00})) {
|
||||
editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "transparency").apply();
|
||||
} else {
|
||||
LOG.warn("Unknown audio mode. Payload: " + hexdump(payload));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] encodeAudioMode(String desired) {
|
||||
byte[] payload = new byte[]{0x01, 0x05, 0x00};
|
||||
|
||||
switch (desired) {
|
||||
case "anc":
|
||||
payload[1] = 0x01;
|
||||
break;
|
||||
case "anc-light":
|
||||
payload[1] = 0x03;
|
||||
break;
|
||||
case "transparency":
|
||||
payload[1] = 0x07;
|
||||
break;
|
||||
case "off":
|
||||
default:
|
||||
}
|
||||
return encodeMessage((short) 0x120, audio_mode, payload);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] encodeFindDevice(boolean start) {
|
||||
byte payload = (byte) (start ? 0x01 : 0x00);
|
||||
return encodeMessage((short) 0x120, find_device, new byte[]{payload});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSendConfiguration(String config) {
|
||||
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR:
|
||||
byte enabled = (byte) (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR, true) ? 0x01 : 0x00);
|
||||
return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
|
||||
// response: 55 20 01 04 70 00 00 00
|
||||
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE:
|
||||
return encodeAudioMode(prefs.getString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off"));
|
||||
// response: 55 20 01 0F 70 00 00 00
|
||||
|
||||
default:
|
||||
LOG.debug("CONFIG: " + config);
|
||||
}
|
||||
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSetTime() {
|
||||
// This are earphones, there is no time to set here. However this method gets called soon
|
||||
// after connecting, hence we use it to perform some initializations.
|
||||
// TODO: Find a way to send more requests during the first connection
|
||||
return encodeAudioModeStatusReq();
|
||||
}
|
||||
|
||||
private List<GBDeviceEvent> handleBatteryInfo(byte[] payload) {
|
||||
List<GBDeviceEvent> batEvts = new ArrayList<>();
|
||||
|
||||
//LOG.debug("Battery payload: " + hexdump(payload));
|
||||
|
||||
/* payload:
|
||||
1st byte is number of batteries, then $number pairs follow:
|
||||
{idx, value}
|
||||
|
||||
idx is 0x02 for left ear, 0x03 for right ear, 0x04 for case
|
||||
value goes from 0-64 (equivalent of 0-100 in hexadecimal)
|
||||
|
||||
|
||||
Since Gadgetbridge supports only one battery, we use an average of the levels for the
|
||||
battery level.
|
||||
If one of the batteries is recharging, we consider the battery as recharging.
|
||||
*/
|
||||
|
||||
// GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo();
|
||||
// evBattery.level = 0;
|
||||
// boolean batteryCharging = false;
|
||||
|
||||
int numBatteries = payload[0];
|
||||
for (int i = 0; i < numBatteries; i++) {
|
||||
|
||||
batteries.get(payload[1 + 2 * i]).level = (payload[2 + 2 * i] & MASK_BATTERY);
|
||||
batteries.get(payload[1 + 2 * i]).state =
|
||||
((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING) ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
|
||||
|
||||
batEvts.add(batteries.get(payload[1 + 2 * i]));
|
||||
|
||||
// evBattery.level += (short) ((payload[2 + 2 * i] & MASK_BATTERY) / numBatteries);
|
||||
// if (!batteryCharging) {
|
||||
// batteryCharging = ((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING);
|
||||
// }
|
||||
// LOG.debug("single battery level: " + hexdump(payload, 2+2*i,1) +"-"+ ((payload[2+2*i] & 0xff))+":" + evBattery.level);
|
||||
}
|
||||
|
||||
// evBattery.state = BatteryState.UNKNOWN;
|
||||
// evBattery.state = batteryCharging ? BatteryState.BATTERY_CHARGING : evBattery.state;
|
||||
|
||||
// return evBattery;
|
||||
return batEvts;
|
||||
}
|
||||
|
||||
private short getRequestCommand(short command) {
|
||||
return (short) (command | MASK_REQUEST_CMD);
|
||||
}
|
||||
|
||||
private boolean isOk(short control) {
|
||||
return (control & MASK_RSP_CODE) == 0;
|
||||
}
|
||||
|
||||
private boolean isSupportedDevice(short control) {
|
||||
return getDeviceType(control) == CONTROL_DEVICE_TYPE_TWS_HEADSET;
|
||||
}
|
||||
|
||||
private byte getDeviceType(short control) {
|
||||
return (byte) ((control & MASK_DEVICE_TYPE) >> 8);
|
||||
}
|
||||
|
||||
private AbstractEarCoordinator getCoordinator() {
|
||||
return (AbstractEarCoordinator) getDevice().getDeviceCoordinator();
|
||||
}
|
||||
|
||||
protected NothingProtocol(GBDevice device) {
|
||||
super(device);
|
||||
|
||||
batteries = new HashMap<>(3);
|
||||
|
||||
batteries.put(battery_earphone_left, new GBDeviceEventBatteryInfo());
|
||||
batteries.put(battery_earphone_right, new GBDeviceEventBatteryInfo());
|
||||
batteries.put(battery_case, new GBDeviceEventBatteryInfo());
|
||||
|
||||
batteries.get(battery_case).batteryIndex=0;
|
||||
batteries.get(battery_earphone_left).batteryIndex=1;
|
||||
batteries.get(battery_earphone_right).batteryIndex=2;
|
||||
|
||||
incrementCounter = getCoordinator().incrementCounter();
|
||||
}
|
||||
}
|
||||
@@ -3528,7 +3528,7 @@
|
||||
|
||||
<string-array name="nothing_ear1_audio_mode_values">
|
||||
<item>anc</item>
|
||||
<item>anc-light</item>
|
||||
<item>anclight</item>
|
||||
<item>transparency</item>
|
||||
<item>off</item>
|
||||
</string-array>
|
||||
|
||||
Reference in New Issue
Block a user