package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty; import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; import android.content.SharedPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; 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.sony.headphones.prefs.AmbientSoundControlButtonMode; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class SoundcoreLibertyProtocol extends GBDeviceProtocol { private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyProtocol.class); private static final int battery_case = 0; private static final int battery_earphone_left = 1; private static final int battery_earphone_right = 2; protected SoundcoreLibertyProtocol(GBDevice device) { super(device); } private GBDeviceEventBatteryInfo buildBatteryInfo(int batteryIndex, int level) { GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo(); info.batteryIndex = batteryIndex; info.level = level; return info; } private GBDeviceEventVersionInfo buildVersionInfo(String firmware1, String firmware2, String serialNumber) { GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo(); info.hwVersion = serialNumber; info.fwVersion = firmware1; info.fwVersion2 = firmware2; return info; } private String readString(byte[] data, int position, int size) { if (position + size > data.length) throw new IllegalStateException(); return new String(data, position, size, StandardCharsets.UTF_8); } @Override public GBDeviceEvent[] decodeResponse(byte[] responseData) { ByteBuffer buf = ByteBuffer.wrap(responseData); SoundcorePacket packet = SoundcorePacket.decode(buf); if (packet == null) return null; List devEvts = new ArrayList<>(); short cmd = packet.getCommand(); byte[] payload = packet.getPayload(); if (cmd == (short) 0x0101) { // a lot of other data is in here, anything interesting? String firmware1 = readString(payload, 6, 5); String firmware2 = readString(payload, 11, 5); String serialNumber = readString(payload, 16, 16); devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber)); } else if (cmd == (short) 0x8d01) { LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData)); } else if (cmd == (short) 0x8205) { LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData)); } else if (cmd == (short) 0x0105) { LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData)); } else if (cmd == (short) 0x0106) { //Sound Mode Update decodeAudioMode(payload); } else if (cmd == (short) 0x0301) { // Battery Update int batteryLeft = payload[0] * 20; int batteryRight = payload[1] * 20; int batteryCase = payload[2] * 20; devEvts.add(buildBatteryInfo(battery_case, batteryCase)); devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeft)); devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRight)); } else { // see https://github.com/gmallios/SoundcoreManager/blob/master/soundcore-lib/src/models/packet_kind.rs // for a mapping for other soundcore devices (similar protocol?) LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData)); } return devEvts.toArray(new GBDeviceEvent[devEvts.size()]); } private void decodeAudioMode(byte[] payload) { SharedPreferences prefs = getDevicePrefs().getPreferences(); SharedPreferences.Editor editor = prefs.edit(); String soundmode = "off"; int anc_strength = 0; if (payload[0] == 0x00) { soundmode = "noise_cancelling"; } else if (payload[0] == 0x01) { soundmode = "ambient_sound"; } else if (payload[0] == 0x02) { soundmode = "off"; } if (payload[1] == 0x10) { anc_strength = 0; } else if (payload[1] == 0x20) { anc_strength = 1; } else if (payload[1] == 0x30) { anc_strength = 2; } boolean vocal_mode = (payload[2] == 0x01); boolean adaptive_anc = (payload[3] == 0x01); boolean windnoiseReduction = (payload[4] == 0x01); editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode); editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength); editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode); editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc); editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, windnoiseReduction); editor.apply(); } @Override public byte[] encodeSendConfiguration(String config) { Prefs prefs = getDevicePrefs(); String pref_string; switch (config) { // Ambient Sound Modes case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL: case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION: case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE: case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING: case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL: return encodeAudioMode(); // Control case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED: return encodeControlTouchLockMessage(TapAction.SINGLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED, false)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED: return encodeControlTouchLockMessage(TapAction.DOUBLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED, false)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED: return encodeControlTouchLockMessage(TapAction.TRIPLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED, false)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED: return encodeControlTouchLockMessage(TapAction.LONG_PRESS, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED, false)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT, ""); return encodeControlFunctionMessage(TapAction.SINGLE_TAP, false, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT, ""); return encodeControlFunctionMessage(TapAction.SINGLE_TAP, true, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT, ""); return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, false, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT, ""); return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, true, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT, ""); return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, false, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT, ""); return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, true, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT, ""); return encodeControlFunctionMessage(TapAction.LONG_PRESS, false, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT: pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT, ""); return encodeControlFunctionMessage(TapAction.LONG_PRESS, true, TapFunction.valueOf(pref_string)); case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE: AmbientSoundControlButtonMode modes = AmbientSoundControlButtonMode.fromPreferences(prefs.getPreferences()); switch (modes) { case NC_AS_OFF: return encodeControlAmbientModeMessage(true, true, true); case NC_AS: return encodeControlAmbientModeMessage(true, true, false); case NC_OFF: return encodeControlAmbientModeMessage(true, false, true); case AS_OFF: return encodeControlAmbientModeMessage(false, true, true); } // Miscellaneous Settings case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION: boolean wearingDetection = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION, false); return new SoundcorePacket((short) 0x8101, new byte[]{encodeBoolean(wearingDetection)}).encode(); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE: boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false); return new SoundcorePacket((short) 0x8c01, new byte[]{encodeBoolean(wearingTone)}).encode(); case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE: boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false); return new SoundcorePacket((short) 0x8301, new byte[]{encodeBoolean(touchTone)}).encode(); default: LOG.debug("Unsupported CONFIG: " + config); } return super.encodeSendConfiguration(config); } byte[] encodeDeviceInfoRequest() { return new SoundcorePacket((short) 0x0101).encode(); } byte[] encodeMysteryDataRequest1() { byte[] payload = new byte[]{0x00}; return new SoundcorePacket((short) 0x8d01, payload).encode(); } byte[] encodeMysteryDataRequest2() { return new SoundcorePacket((short) 0x0105).encode(); } byte[] encodeMysteryDataRequest3() { byte[] payload = new byte[]{0x00}; return new SoundcorePacket((short) 0x8205, payload).encode(); } /** * Encodes the following settings to a payload to set the audio-mode on the headphones: * PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active * PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise * PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode * PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent * PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise * @return The payload */ private byte[] encodeAudioMode() { Prefs prefs = getDevicePrefs(); byte anc_mode; switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) { case "noise_cancelling": anc_mode = 0x00; break; case "ambient_sound": anc_mode = 0x01; break; case "off": anc_mode = 0x02; break; default: LOG.error("Invalid Audio Mode selected"); return null; } byte anc_strength; switch (prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0)) { case 0: anc_strength = 0x10; break; case 1: anc_strength = 0x20; break; case 2: anc_strength = 0x30; break; default: LOG.error("Invalid ANC Strength selected"); return null; } byte adaptive_anc = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, true)); byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false)); byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false)); byte[] payload = new byte[]{anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01}; return new SoundcorePacket((short) 0x8106, payload).encode(); } /** * Enables or disables a tap-action * @param action The byte that encodes the action (single/double/triple or long tap) * @param disabled If the action should be enabled or disabled * @return */ private byte[] encodeControlTouchLockMessage(TapAction action, boolean disabled) { boolean enabled = !disabled; byte enabled_byte; byte[] payload; switch (action) { case SINGLE_TAP: case TRIPLE_TAP: enabled_byte = encodeBoolean(enabled); break; case DOUBLE_TAP: case LONG_PRESS: enabled_byte = enabled?(byte) 0x11: (byte) 0x10; break; default: LOG.error("Invalid Tap action"); return null; } payload = new byte[]{0x00, action.getCode(), enabled_byte}; return new SoundcorePacket((short) 0x8304, payload).encode(); } /** * Assigns a function (eg play/pause) to an action (eg single tap on right bud) * @param action The byte that encodes the action (single/double/triple or long tap) * @param right If the right or left earbud is meant * @param function The byte that encodes the triggered function (eg play/pause) * @return The encoded message */ private byte[] encodeControlFunctionMessage(TapAction action, boolean right, TapFunction function) { byte function_byte; switch (action) { case SINGLE_TAP: case DOUBLE_TAP: function_byte = (byte) (16*6 + function.getCode()); break; case TRIPLE_TAP: function_byte = (byte) (16*4 + function.getCode()); break; case LONG_PRESS: function_byte = (byte) (16*5 + function.getCode()); break; default: LOG.error("Invalid Tap action"); return null; } byte[] payload = new byte[] {encodeBoolean(right), action.getCode(), function_byte}; return new SoundcorePacket((short) 0x8104, payload).encode(); } /** * Encodes between which Audio Modes a tap should switch, if it is set to switch the Audio Mode. * Zb ANC -> -> Transparency -> Normal -> ANC -> .... */ private byte[] encodeControlAmbientModeMessage(boolean anc, boolean transparency, boolean normal) { // Original app does not allow only one true flag. Unsure if Earbuds accept this state. byte ambientModes = (byte) (4 * (normal?1:0) + 2 * (transparency?1:0) + (anc?1:0)); return new SoundcorePacket((short) 0x8206, new byte[] {ambientModes}).encode(); } private byte encodeBoolean(boolean bool) { if (bool) return 0x01; else return 0x00; } }