1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-21 04:20:27 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocol.java
2021-11-02 21:15:25 +01:00

311 lines
12 KiB
Java

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.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 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(control);
msgBuf.putShort(command);
msgBuf.putShort((short) payload.length);
msgBuf.put((byte) 0x00); //fsn TODO: is this always 0?
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);
}
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;
}
}