Sony Headphones: Merge V3 protocol into V2

The protocols are not actually different - the different messages only
depended on whether the devices supports wind noise reduction or not.
This commit is contained in:
José Rebelo 2024-03-15 22:56:11 +00:00
parent f91156cd3e
commit 0301d0e9a8
8 changed files with 452 additions and 821 deletions

View File

@ -16,8 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
@ -25,9 +23,7 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class SonyWF1000XM4Coordinator extends SonyHeadphonesCoordinator {
@Override

View File

@ -54,7 +54,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.prot
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.AbstractSonyProtocolImpl;
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.devices.sony.headphones.protocol.impl.v3.SonyProtocolImplV3;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SonyHeadphonesProtocol extends GBDeviceProtocol {
@ -123,21 +122,11 @@ public class SonyHeadphonesProtocol extends GBDeviceProtocol {
// Wi-SP600N: 01:00:40:10
protocolVersion = "v1";
} else if (message.getPayload().length == 8) {
switch (message.getPayload()[2]) {
case 0x01:
// WF-1000XM4 1.1.5: 01:00:01:00:00:00:00:00
protocolVersion = "v2";
break;
case 0x03:
// LinkBuds S 2.0.2: 01:00:03:00:00:07:00:00
// WH-1000XM5 1.1.3: 01:00:03:00:00:00:00:00
// WF-1000XM5 2.0.1: 01:00:03:00:10:04:00:00
protocolVersion = "v3";
break;
default:
LOG.error("Unexpected version for payload of length 8: {}", message.getPayload()[2]);
return events.toArray(new GBDeviceEvent[0]);
}
// WF-1000XM4 1.1.5: 01:00:01:00:00:00:00:00
// LinkBuds S 2.0.2: 01:00:03:00:00:07:00:00
// WH-1000XM5 1.1.3: 01:00:03:00:00:00:00:00
// WF-1000XM5 2.0.1: 01:00:03:00:10:04:00:00
protocolVersion = "v3";
} else {
LOG.error("Unexpected init response payload length: {}", message.getPayload().length);
return events.toArray(new GBDeviceEvent[0]);
@ -160,9 +149,6 @@ public class SonyHeadphonesProtocol extends GBDeviceProtocol {
case "v2":
protocolImpl = new SonyProtocolImplV2(getDevice());
break;
case "v3":
protocolImpl = new SonyProtocolImplV3(getDevice());
break;
default:
LOG.warn("Unknown protocol version {}", protocolVersion);
return events.toArray(new GBDeviceEvent[0]);

View File

@ -33,6 +33,11 @@ public enum PayloadTypeV2 {
AUTOMATIC_POWER_OFF_SET(MessageType.COMMAND_1, 0x28),
AUTOMATIC_POWER_OFF_NOTIFY(MessageType.COMMAND_1, 0x29),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET(MessageType.COMMAND_1, 0xfa),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_RET(MessageType.COMMAND_1, 0xfb),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET(MessageType.COMMAND_1, 0xfc),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_NOTIFY(MessageType.COMMAND_1, 0xfd),
UNKNOWN(MessageType.UNKNOWN, 0xff);
private final MessageType messageType;

View File

@ -66,17 +66,17 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getCode(),
(byte) 0x15
(byte) (supportsWindNoiseCancelling() ? 0x17 : 0x15)
}
);
}
@Override
public Request setAmbientSoundControl(final AmbientSoundControl ambientSoundControl) {
final ByteBuffer buf = ByteBuffer.allocate(8);
final ByteBuffer buf = ByteBuffer.allocate(supportsWindNoiseCancelling() ? 8 : 7);
buf.put(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getCode());
buf.put((byte) 0x15);
buf.put((byte) (supportsWindNoiseCancelling() ? 0x17 : 0x15));
buf.put((byte) 0x01); // 0x00 while dragging the slider?
if (AmbientSoundControl.Mode.OFF.equals(ambientSoundControl.getMode())) {
@ -91,10 +91,12 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
buf.put((byte) 0x00);
}
if (AmbientSoundControl.Mode.WIND_NOISE_REDUCTION.equals(ambientSoundControl.getMode())) {
buf.put((byte) 0x03);
} else {
buf.put((byte) 0x02);
if (supportsWindNoiseCancelling()) {
if (AmbientSoundControl.Mode.WIND_NOISE_REDUCTION.equals(ambientSoundControl.getMode())) {
buf.put((byte) 0x03);
} else {
buf.put((byte) 0x02);
}
}
buf.put((byte) (ambientSoundControl.isFocusOnVoice() ? 0x01 : 0x00));
@ -105,26 +107,50 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public Request setSpeakToChatEnabled(SpeakToChatEnabled config) {
LOG.warn("Speak-to-chat not implemented for V2");
return null;
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(),
(byte) 0x0c,
(byte) (config.isEnabled() ? 0x00 : 0x01), // TODO it's reversed?
(byte) 0x01
}
);
}
@Override
public Request getSpeakToChatEnabled() {
LOG.warn("Speak-to-chat not implemented for V2");
return null;
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(),
(byte) 0x0c
}
);
}
@Override
public Request setSpeakToChatConfig(SpeakToChatConfig config) {
LOG.warn("Speak-to-chat not implemented for V2");
return null;
return new Request(
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_SET.getMessageType(),
new byte[]{
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_SET.getCode(),
(byte) 0x0c,
config.getSensitivity().getCode(),
config.getTimeout().getCode()
}
);
}
@Override
public Request getSpeakToChatConfig() {
LOG.warn("Speak-to-chat not implemented for V2");
return null;
return new Request(
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_GET.getMessageType(),
new byte[]{
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_GET.getCode(),
(byte) 0x0c
}
);
}
@Override
@ -234,26 +260,54 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public Request getQuickAccess() {
LOG.warn("Quick access not implemented for V2");
return null;
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(),
(byte) 0x0d
}
);
}
@Override
public Request setQuickAccess(final QuickAccess quickAccess) {
LOG.warn("Quick access not implemented for V2");
return null;
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(),
(byte) 0x0d,
(byte) 0x02,
quickAccess.getModeDoubleTap().getCode(),
quickAccess.getModeTripleTap().getCode()
}
);
}
@Override
public Request getAmbientSoundControlButtonMode() {
LOG.warn("Ambient sound control button modes not implemented for V2");
return null;
return new Request(
PayloadTypeV2.AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV2.AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET.getCode(),
(byte) 0x03
}
);
}
@Override
public Request setAmbientSoundControlButtonMode(final AmbientSoundControlButtonMode ambientSoundControlButtonMode) {
LOG.warn("Ambient sound control button modes not implemented for V2");
return null;
return new Request(
PayloadTypeV2.AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV2.AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET.getCode(),
(byte) 0x03,
(byte) 0x01,
(byte) 0x35,
(byte) 0x01,
(byte) 0x00,
ambientSoundControlButtonMode.getCode()
}
);
}
@Override
@ -305,8 +359,22 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public Request setEqualizerCustomBands(final EqualizerCustomBands config) {
LOG.warn("Equalizer custom bands not implemented for V2");
return null;
final ByteBuffer buf = ByteBuffer.allocate(10);
buf.put(PayloadTypeV1.EQUALIZER_SET.getCode());
buf.put((byte) 0x00);
buf.put((byte) 0xa0);
buf.put((byte) 0x06);
buf.put((byte) (config.getBass() + 10));
for (final Integer band : config.getBands()) {
buf.put((byte) (band + 10));
}
return new Request(
PayloadTypeV1.EQUALIZER_SET.getMessageType(),
buf.array()
);
}
@Override
@ -347,14 +415,25 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public Request getVoiceNotifications() {
LOG.warn("Voice notifications not implemented for V2");
return null;
return new Request(
PayloadTypeV1.VOICE_NOTIFICATIONS_GET.getMessageType(),
new byte[]{
PayloadTypeV1.VOICE_NOTIFICATIONS_GET.getCode(),
(byte) 0x01
}
);
}
@Override
public Request setVoiceNotifications(final VoiceNotifications config) {
LOG.warn("Voice notifications not implemented for V2");
return null;
return new Request(
PayloadTypeV1.VOICE_NOTIFICATIONS_SET.getMessageType(),
new byte[]{
PayloadTypeV1.VOICE_NOTIFICATIONS_SET.getCode(),
(byte) 0x01,
(byte) (config.isEnabled() ? 0x00 : 0x01) // reversed?
}
);
}
@Override
@ -401,6 +480,9 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
case AUTOMATIC_POWER_OFF_RET:
case AUTOMATIC_POWER_OFF_NOTIFY:
return handleAutomaticPowerOff(payload);
case AMBIENT_SOUND_CONTROL_BUTTON_MODE_RET:
case AMBIENT_SOUND_CONTROL_BUTTON_MODE_NOTIFY:
return handleAmbientSoundControlButtonMode(payload);
}
return super.handlePayload(messageType, payload);
@ -413,16 +495,18 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public List<? extends GBDeviceEvent> handleAmbientSoundControl(final byte[] payload) {
if (payload.length != 8) {
if (payload.length != 8 && payload.length != 7) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x15) {
if (payload[1] != 0x15 && payload[1] != 0x17) {
LOG.warn("Not ambient sound control, ignoring {}", payload[1]);
return Collections.emptyList();
}
final boolean includesWindNoiseReduction = payload[1] == 0x17;
AmbientSoundControl.Mode mode = null;
if (payload[3] == (byte) 0x00) {
@ -430,9 +514,17 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
} else if (payload[3] == (byte) 0x01) {
// Enabled, determine mode
if (payload[5] == 0x03 || payload[5] == 0x05) {
mode = AmbientSoundControl.Mode.WIND_NOISE_REDUCTION;
} else if (payload[5] == 0x02) {
if (includesWindNoiseReduction) {
if (payload[5] == 0x03 || payload[5] == 0x05) {
mode = AmbientSoundControl.Mode.WIND_NOISE_REDUCTION;
} else if (payload[5] == 0x02) {
if (payload[4] == (byte) 0x00) {
mode = AmbientSoundControl.Mode.NOISE_CANCELLING;
} else if (payload[4] == (byte) 0x01) {
mode = AmbientSoundControl.Mode.AMBIENT_SOUND;
}
}
} else {
if (payload[4] == (byte) 0x00) {
mode = AmbientSoundControl.Mode.NOISE_CANCELLING;
} else if (payload[4] == (byte) 0x01) {
@ -446,15 +538,17 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
return Collections.emptyList();
}
final Boolean focusOnVoice = booleanFromByte(payload[6]);
int i = includesWindNoiseReduction ? 6 : 5;
final Boolean focusOnVoice = booleanFromByte(payload[i]);
if (focusOnVoice == null) {
LOG.warn("Unknown focus on voice mode {}", String.format("%02x", payload[6]));
LOG.warn("Unknown focus on voice mode {}", String.format("%02x", payload[i]));
return Collections.emptyList();
}
int ambientSound = payload[7];
i++;
int ambientSound = payload[i];
if (ambientSound < 0 || ambientSound > 20) {
LOG.warn("Ambient sound level {} is out of range", String.format("%02x", payload[7]));
LOG.warn("Ambient sound level {} is out of range", String.format("%02x", payload[i]));
return Collections.emptyList();
}
@ -533,6 +627,86 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
return Collections.singletonList(event);
}
@Override
public List<? extends GBDeviceEvent> handleSpeakToChatEnabled(final byte[] payload) {
if (payload.length != 4) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x0c) {
LOG.warn("Not speak to chat enabled, ignoring");
return Collections.emptyList();
}
final Boolean disabled = booleanFromByte(payload[2]);
if (disabled == null) {
LOG.warn("Unknown speak to chat enabled code {}", String.format("%02x", payload[2]));
return Collections.emptyList();
}
LOG.debug("Speak to chat: {}", !disabled);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new SpeakToChatEnabled(!disabled).toPreferences());
return Collections.singletonList(event);
}
public List<? extends GBDeviceEvent> handleQuickAccess(final byte[] payload) {
if (payload.length != 5) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x0d || payload[2] != 0x02) {
LOG.warn("Unexpected quick access payload bytes {}", String.format("%02x %02x", payload[1], payload[2]));
return Collections.emptyList();
}
final QuickAccess.Mode modeDouble = QuickAccess.Mode.fromCode(payload[3]);
final QuickAccess.Mode modeTriple = QuickAccess.Mode.fromCode(payload[4]);
if (modeDouble == null || modeTriple == null) {
LOG.warn("Unknown quick access codes {}", String.format("%02x %02x", payload[3], payload[4]));
return Collections.emptyList();
}
LOG.debug("Quick Access: Double Tap: {}, Triple Tap: {}", modeDouble, modeTriple);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new QuickAccess(modeDouble, modeTriple).toPreferences());
return Collections.singletonList(event);
}
public List<? extends GBDeviceEvent> handleAmbientSoundControlButtonMode(final byte[] payload) {
if (payload.length != 7) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x03 || payload[2] != 0x01 || payload[3] != 0x35 || payload[4] != 0x01 || payload[5] != 0x00) {
LOG.warn(
"Unexpected ambient sound control button mode payload bytes {}",
String.format("%02x %02x %02x %02x %02x", payload[1], payload[2], payload[3], payload[4], payload[5])
);
return Collections.emptyList();
}
final AmbientSoundControlButtonMode mode = AmbientSoundControlButtonMode.fromCode(payload[6]);
if (mode == null) {
LOG.warn("Unknown ambient sound control button mode code {}", String.format("%02x", payload[6]));
return Collections.emptyList();
}
LOG.debug("Ambient Sound Control Buton Mode: {}", mode);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(mode.toPreferences());
return Collections.singletonList(event);
}
@Override
public List<? extends GBDeviceEvent> handleButtonModes(final byte[] payload) {
if (payload.length != 5) {
@ -641,6 +815,10 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
return handlePauseWhenTakenOff(payload);
case 0x03:
return handleButtonModes(payload);
case 0x0c:
return handleSpeakToChatEnabled(payload);
case 0x0d:
return handleQuickAccess(payload);
}
return Collections.emptyList();
@ -672,8 +850,32 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
@Override
public List<? extends GBDeviceEvent> handleVoiceNotifications(final byte[] payload) {
LOG.warn("Voice notifications not implemented for V2");
return Collections.emptyList();
if (payload.length != 4) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
boolean enabled;
// reversed?
switch (payload[2]) {
case 0x00:
enabled = true;
break;
case 0x01:
enabled = false;
break;
default:
LOG.warn("Unknown voice notifications code {}", String.format("%02x", payload[3]));
return Collections.emptyList();
}
LOG.debug("Voice Notifications: {}", enabled);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new VoiceNotifications(enabled).toPreferences());
return Collections.singletonList(event);
}
@Override
@ -703,4 +905,37 @@ public class SonyProtocolImplV2 extends SonyProtocolImplV1 {
throw new IllegalArgumentException("Unknown battery type " + batteryType);
}
@Override
protected ButtonModes.Mode decodeButtonMode(final byte b) {
switch (b) {
case (byte) 0xff:
return ButtonModes.Mode.OFF;
case (byte) 0x00:
case (byte) 0x35: // Seems to be the only one that differs?
return ButtonModes.Mode.AMBIENT_SOUND_CONTROL;
case (byte) 0x20:
return ButtonModes.Mode.PLAYBACK_CONTROL;
case (byte) 0x10:
return ButtonModes.Mode.VOLUME_CONTROL;
}
return null;
}
@Override
protected byte encodeButtonMode(final ButtonModes.Mode buttonMode) {
switch (buttonMode) {
case OFF:
return (byte) 0xff;
case AMBIENT_SOUND_CONTROL:
return (byte) (supportsWindNoiseCancelling() ? 0x35 : 0x00); // Seems to be the only one that differs?
case PLAYBACK_CONTROL:
return (byte) 0x20;
case VOLUME_CONTROL:
return (byte) 0x10;
}
throw new IllegalArgumentException("Unknown button mode " + buttonMode);
}
}

View File

@ -1,54 +0,0 @@
/* Copyright (C) 2022-2024 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.protocol.impl.v3;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType;
public enum PayloadTypeV3 {
AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET(MessageType.COMMAND_1, 0xfa),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_RET(MessageType.COMMAND_1, 0xfb),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET(MessageType.COMMAND_1, 0xfc),
AMBIENT_SOUND_CONTROL_BUTTON_MODE_NOTIFY(MessageType.COMMAND_1, 0xfd),
UNKNOWN(MessageType.UNKNOWN, 0xff);
private final MessageType messageType;
private final byte code;
PayloadTypeV3(final MessageType messageType, final int code) {
this.messageType = messageType;
this.code = (byte) code;
}
public MessageType getMessageType() {
return this.messageType;
}
public byte getCode() {
return this.code;
}
public static PayloadTypeV3 fromCode(final MessageType messageType, final byte code) {
for (final PayloadTypeV3 payloadType : values()) {
if (messageType.equals(payloadType.messageType) && payloadType.code == code) {
return payloadType;
}
}
return PayloadTypeV3.UNKNOWN;
}
}

View File

@ -1,447 +0,0 @@
/* Copyright (C) 2022-2024 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.protocol.impl.v3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.ButtonModes;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.QuickAccess;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatEnabled;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.PayloadTypeV1;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2.SonyProtocolImplV2;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class SonyProtocolImplV3 extends SonyProtocolImplV2 {
private static final Logger LOG = LoggerFactory.getLogger(SonyProtocolImplV3.class);
public SonyProtocolImplV3(final GBDevice device) {
super(device);
}
@Override
public Request getAmbientSoundControl() {
return new Request(
PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getCode(),
(byte) 0x17
}
);
}
@Override
public Request setAmbientSoundControl(final AmbientSoundControl ambientSoundControl) {
final ByteBuffer buf = ByteBuffer.allocate(7);
buf.put(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getCode());
buf.put((byte) 0x17);
buf.put((byte) 0x01); // 0x00 while dragging the slider?
if (AmbientSoundControl.Mode.OFF.equals(ambientSoundControl.getMode())) {
buf.put((byte) 0x00);
} else {
buf.put((byte) 0x01);
}
if (AmbientSoundControl.Mode.AMBIENT_SOUND.equals(ambientSoundControl.getMode())) {
buf.put((byte) 0x01);
} else {
buf.put((byte) 0x00);
}
buf.put((byte) (ambientSoundControl.isFocusOnVoice() ? 0x01 : 0x00));
buf.put((byte) (ambientSoundControl.getAmbientSound()));
return new Request(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getMessageType(), buf.array());
}
@Override
public Request setSpeakToChatEnabled(final SpeakToChatEnabled config) {
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(),
(byte) 0x0c,
(byte) (config.isEnabled() ? 0x00 : 0x01), // TODO it's reversed?
(byte) 0x01
}
);
}
@Override
public Request getSpeakToChatEnabled() {
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(),
(byte) 0x0c
}
);
}
@Override
public Request setSpeakToChatConfig(final SpeakToChatConfig config) {
return new Request(
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_SET.getMessageType(),
new byte[]{
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_SET.getCode(),
(byte) 0x0c,
config.getSensitivity().getCode(),
config.getTimeout().getCode()
}
);
}
@Override
public Request getSpeakToChatConfig() {
return new Request(
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_GET.getMessageType(),
new byte[]{
PayloadTypeV1.SPEAK_TO_CHAT_CONFIG_GET.getCode(),
(byte) 0x0c
}
);
}
@Override
public Request getQuickAccess() {
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(),
(byte) 0x0d
}
);
}
@Override
public Request setQuickAccess(final QuickAccess quickAccess) {
return new Request(
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(),
(byte) 0x0d,
(byte) 0x02,
quickAccess.getModeDoubleTap().getCode(),
quickAccess.getModeTripleTap().getCode()
}
);
}
@Override
public Request getAmbientSoundControlButtonMode() {
return new Request(
PayloadTypeV3.AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET.getMessageType(),
new byte[]{
PayloadTypeV3.AMBIENT_SOUND_CONTROL_BUTTON_MODE_GET.getCode(),
(byte) 0x03
}
);
}
@Override
public Request setAmbientSoundControlButtonMode(final AmbientSoundControlButtonMode ambientSoundControlButtonMode) {
return new Request(
PayloadTypeV3.AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET.getMessageType(),
new byte[]{
PayloadTypeV3.AMBIENT_SOUND_CONTROL_BUTTON_MODE_SET.getCode(),
(byte) 0x03,
(byte) 0x01,
(byte) 0x35,
(byte) 0x01,
(byte) 0x00,
ambientSoundControlButtonMode.getCode()
}
);
}
@Override
public Request setEqualizerCustomBands(final EqualizerCustomBands config) {
final ByteBuffer buf = ByteBuffer.allocate(10);
buf.put(PayloadTypeV1.EQUALIZER_SET.getCode());
buf.put((byte) 0x00);
buf.put((byte) 0xa0);
buf.put((byte) 0x06);
buf.put((byte) (config.getBass() + 10));
for (final Integer band : config.getBands()) {
buf.put((byte) (band + 10));
}
return new Request(
PayloadTypeV1.EQUALIZER_SET.getMessageType(),
buf.array()
);
}
@Override
public Request getVoiceNotifications() {
return new Request(
PayloadTypeV1.VOICE_NOTIFICATIONS_GET.getMessageType(),
new byte[]{
PayloadTypeV1.VOICE_NOTIFICATIONS_GET.getCode(),
(byte) 0x01
}
);
}
@Override
public Request setVoiceNotifications(final VoiceNotifications config) {
return new Request(
PayloadTypeV1.VOICE_NOTIFICATIONS_SET.getMessageType(),
new byte[]{
PayloadTypeV1.VOICE_NOTIFICATIONS_SET.getCode(),
(byte) 0x01,
(byte) (config.isEnabled() ? 0x00 : 0x01) // reversed?
}
);
}
@Override
public List<? extends GBDeviceEvent> handlePayload(final MessageType messageType, final byte[] payload) {
final PayloadTypeV3 payloadType = PayloadTypeV3.fromCode(messageType, payload[0]);
switch (payloadType) {
case AMBIENT_SOUND_CONTROL_BUTTON_MODE_RET:
case AMBIENT_SOUND_CONTROL_BUTTON_MODE_NOTIFY:
return handleAmbientSoundControlButtonMode(payload);
}
return super.handlePayload(messageType, payload);
}
@Override
public List<? extends GBDeviceEvent> handleAmbientSoundControl(final byte[] payload) {
if (payload.length != 7) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x17) {
LOG.warn("Not ambient sound control, ignoring {}", payload[1]);
return Collections.emptyList();
}
AmbientSoundControl.Mode mode = null;
if (payload[3] == (byte) 0x00) {
mode = AmbientSoundControl.Mode.OFF;
} else if (payload[3] == (byte) 0x01) {
// Enabled, determine mode
if (payload[4] == (byte) 0x00) {
mode = AmbientSoundControl.Mode.NOISE_CANCELLING;
} else if (payload[4] == (byte) 0x01) {
mode = AmbientSoundControl.Mode.AMBIENT_SOUND;
}
}
if (mode == null) {
LOG.warn("Unable to determine ambient sound control mode from {}", GB.hexdump(payload));
return Collections.emptyList();
}
final Boolean focusOnVoice = booleanFromByte(payload[5]);
if (focusOnVoice == null) {
LOG.warn("Unknown focus on voice mode {}", String.format("%02x", payload[5]));
return Collections.emptyList();
}
int ambientSound = payload[6];
if (ambientSound < 0 || ambientSound > 20) {
LOG.warn("Ambient sound level {} is out of range", String.format("%02x", payload[6]));
return Collections.emptyList();
}
final AmbientSoundControl ambientSoundControl = new AmbientSoundControl(mode, focusOnVoice, ambientSound);
LOG.debug("Ambient sound control: {}", ambientSoundControl);
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
.withPreferences(ambientSoundControl.toPreferences());
return Collections.singletonList(eventUpdatePreferences);
}
public List<? extends GBDeviceEvent> handleQuickAccess(final byte[] payload) {
if (payload.length != 5) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x0d || payload[2] != 0x02) {
LOG.warn("Unexpected quick access payload bytes {}", String.format("%02x %02x", payload[1], payload[2]));
return Collections.emptyList();
}
final QuickAccess.Mode modeDouble = QuickAccess.Mode.fromCode(payload[3]);
final QuickAccess.Mode modeTriple = QuickAccess.Mode.fromCode(payload[4]);
if (modeDouble == null || modeTriple == null) {
LOG.warn("Unknown quick access codes {}", String.format("%02x %02x", payload[3], payload[4]));
return Collections.emptyList();
}
LOG.debug("Quick Access: Double Tap: {}, Triple Tap: {}", modeDouble, modeTriple);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new QuickAccess(modeDouble, modeTriple).toPreferences());
return Collections.singletonList(event);
}
public List<? extends GBDeviceEvent> handleAmbientSoundControlButtonMode(final byte[] payload) {
if (payload.length != 7) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x03 || payload[2] != 0x01 || payload[3] != 0x35 || payload[4] != 0x01 || payload[5] != 0x00) {
LOG.warn(
"Unexpected ambient sound control button mode payload bytes {}",
String.format("%02x %02x %02x %02x %02x", payload[1], payload[2], payload[3], payload[4], payload[5])
);
return Collections.emptyList();
}
final AmbientSoundControlButtonMode mode = AmbientSoundControlButtonMode.fromCode(payload[6]);
if (mode == null) {
LOG.warn("Unknown ambient sound control button mode code {}", String.format("%02x", payload[6]));
return Collections.emptyList();
}
LOG.debug("Ambient Sound Control Buton Mode: {}", mode);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(mode.toPreferences());
return Collections.singletonList(event);
}
@Override
public List<? extends GBDeviceEvent> handleSpeakToChatEnabled(final byte[] payload) {
if (payload.length != 4) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
if (payload[1] != 0x0c) {
LOG.warn("Not speak to chat enabled, ignoring");
return Collections.emptyList();
}
final Boolean disabled = booleanFromByte(payload[2]);
if (disabled == null) {
LOG.warn("Unknown speak to chat enabled code {}", String.format("%02x", payload[2]));
return Collections.emptyList();
}
LOG.debug("Speak to chat: {}", !disabled);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new SpeakToChatEnabled(!disabled).toPreferences());
return Collections.singletonList(event);
}
public List<? extends GBDeviceEvent> handleAutomaticPowerOffButtonMode(final byte[] payload) {
switch (payload[1]) {
case 0x0c:
return handleSpeakToChatEnabled(payload);
case 0x0d:
return handleQuickAccess(payload);
}
return Collections.emptyList();
}
public List<? extends GBDeviceEvent> handleVoiceNotifications(final byte[] payload) {
if (payload.length != 4) {
LOG.warn("Unexpected payload length {}", payload.length);
return Collections.emptyList();
}
boolean enabled;
// reversed?
switch (payload[2]) {
case 0x00:
enabled = true;
break;
case 0x01:
enabled = false;
break;
default:
LOG.warn("Unknown voice notifications code {}", String.format("%02x", payload[3]));
return Collections.emptyList();
}
LOG.debug("Voice Notifications: {}", enabled);
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreferences(new VoiceNotifications(enabled).toPreferences());
return Collections.singletonList(event);
}
@Override
protected ButtonModes.Mode decodeButtonMode(final byte b) {
switch (b) {
case (byte) 0xff:
return ButtonModes.Mode.OFF;
case (byte) 0x35: // Seems to be the only one that differs?
return ButtonModes.Mode.AMBIENT_SOUND_CONTROL;
case (byte) 0x20:
return ButtonModes.Mode.PLAYBACK_CONTROL;
case (byte) 0x10:
return ButtonModes.Mode.VOLUME_CONTROL;
}
return null;
}
@Override
protected byte encodeButtonMode(final ButtonModes.Mode buttonMode) {
switch (buttonMode) {
case OFF:
return (byte) 0xff;
case AMBIENT_SOUND_CONTROL:
return (byte) 0x35; // Seems to be the only one that differs?
case PLAYBACK_CONTROL:
return (byte) 0x20;
case VOLUME_CONTROL:
return (byte) 0x10;
}
throw new IllegalArgumentException("Unknown button mode " + buttonMode);
}
}

View File

@ -19,12 +19,15 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.pro
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertPrefs;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequest;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequests;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.handleMessage;
import org.junit.Ignore;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -35,10 +38,18 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePref
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AudioUpsampling;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AutomaticPowerOff;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerPreset;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.PauseWhenTakenOff;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.QuickAccess;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatEnabled;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType;
public class SonyProtocolImplV2Test {
private final SonyProtocolImplV2 protocol = new SonyProtocolImplV2(null) {
@ -56,12 +67,25 @@ public class SonyProtocolImplV2Test {
@Test
public void setAmbientSoundControl() {
// TODO
final Request request = protocol.setAmbientSoundControl(new AmbientSoundControl(
AmbientSoundControl.Mode.WIND_NOISE_REDUCTION,
true,
15
));
assertRequest(request, "3e:0c:00:00:00:00:08:68:15:01:01:00:03:01:0f:a6:3c");
//final Request request = protocol.setAmbientSoundControl(new AmbientSoundControl(
// AmbientSoundControl.Mode.WIND_NOISE_REDUCTION,
// true,
// 15
//));
//assertRequest(request, "3e:0c:00:00:00:00:08:68:15:01:01:00:03:01:0f:a6:3c");
//final Map<AmbientSoundControl, String> commands = new LinkedHashMap<AmbientSoundControl, String>() {{
// put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, false, 20), "68:17:01:01:01:00:14");
// put(new AmbientSoundControl(AmbientSoundControl.Mode.OFF, false, 20), "68:17:01:00:00:00:14");
// put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, false, 10), "68:17:01:01:01:00:0a");
// put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, true, 20), "68:17:01:01:01:01:14");
// put(new AmbientSoundControl(AmbientSoundControl.Mode.NOISE_CANCELLING, false, 20), "68:17:01:01:00:00:14");
//}};
//
//for (Map.Entry<AmbientSoundControl, String> entry : commands.entrySet()) {
// final Request request2 = protocol.setAmbientSoundControl(entry.getKey());
// assertRequest(request2, 0x0c, entry.getValue());
//}
}
@Test
@ -77,7 +101,16 @@ public class SonyProtocolImplV2Test {
@Test
public void getBattery() {
// TODO
final Map<BatteryType, String> commands = new LinkedHashMap<BatteryType, String>() {{
put(BatteryType.SINGLE, "22:00");
put(BatteryType.DUAL, "22:09");
put(BatteryType.CASE, "22:0a");
}};
for (Map.Entry<BatteryType, String> entry : commands.entrySet()) {
final Request request = protocol.getBattery(entry.getKey());
assertRequest(request, 0x0c, entry.getValue());
}
}
@Test
@ -85,6 +118,27 @@ public class SonyProtocolImplV2Test {
// TODO
}
@Test
public void setSpeakToChatEnabled() {
assertRequests(protocol::setSpeakToChatEnabled, new LinkedHashMap<SpeakToChatEnabled, String>() {{
put(new SpeakToChatEnabled(false), "f8:0c:01:01");
put(new SpeakToChatEnabled(true), "f8:0c:00:01");
}});
}
@Test
public void setSpeakToChatConfig() {
assertRequests(protocol::setSpeakToChatConfig, new LinkedHashMap<SpeakToChatConfig, String>() {{
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.HIGH, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:01:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.LOW, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:02:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:00:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.SHORT), "fc:0c:00:00");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.LONG), "fc:0c:00:02");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.OFF), "fc:0c:00:03");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:00:01");
}});
}
@Test
public void getAudioUpsampling() {
// TODO
@ -106,7 +160,10 @@ public class SonyProtocolImplV2Test {
@Test
public void setAutomaticPowerOff() {
// TODO
assertRequests(protocol::setAutomaticPowerOff, new LinkedHashMap<AutomaticPowerOff, String>() {{
put(AutomaticPowerOff.OFF, "28:05:11:00");
put(AutomaticPowerOff.WHEN_TAKEN_OFF, "28:05:10:00");
}});
}
@Test
@ -119,6 +176,47 @@ public class SonyProtocolImplV2Test {
// TODO
}
@Test
public void getQuickAccess() {
final Request request = protocol.getQuickAccess();
assertRequest(request, "3e:0c:00:00:00:00:02:f6:0d:11:3c");
}
@Test
public void setQuickAccess() {
final Map<QuickAccess, String> commands = new LinkedHashMap<QuickAccess, String>() {{
put(new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF), "3e:0c:01:00:00:00:05:f8:0d:02:00:00:19:3c");
put(new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY), "3e:0c:00:00:00:00:05:f8:0d:02:00:01:19:3c");
put(new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF), "3e:0c:00:00:00:00:05:f8:0d:02:01:00:19:3c");
}};
for (Map.Entry<QuickAccess, String> entry : commands.entrySet()) {
final Request request = protocol.setQuickAccess(entry.getKey());
assertRequest(request, entry.getValue());
}
}
@Test
public void getAmbientSoundControlButtonMode() {
final Request request = protocol.getAmbientSoundControlButtonMode();
assertRequest(request, "3e:0c:00:00:00:00:02:fa:03:0b:3c");
}
@Test
public void setAmbientSoundControlButtonMode() {
final Map<AmbientSoundControlButtonMode, String> commands = new LinkedHashMap<AmbientSoundControlButtonMode, String>() {{
put(AmbientSoundControlButtonMode.NC_AS_OFF, "3e:0c:00:00:00:00:07:fc:03:01:35:01:00:01:4a:3c");
put(AmbientSoundControlButtonMode.NC_AS, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:02:4c:3c");
put(AmbientSoundControlButtonMode.NC_OFF, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:03:4d:3c");
put(AmbientSoundControlButtonMode.AS_OFF, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:04:4e:3c");
}};
for (Map.Entry<AmbientSoundControlButtonMode, String> entry : commands.entrySet()) {
final Request request = protocol.setAmbientSoundControlButtonMode(entry.getKey());
assertRequest(request, entry.getValue());
}
}
@Test
public void getPauseWhenTakenOff() {
// TODO
@ -126,11 +224,10 @@ public class SonyProtocolImplV2Test {
@Test
public void setPauseWhenTakenOff() {
final Request requestEnabled = protocol.setPauseWhenTakenOff(new PauseWhenTakenOff(true));
assertRequest(requestEnabled, "3e:0c:01:00:00:00:03:f8:01:00:09:3c");
final Request requestDisabled = protocol.setPauseWhenTakenOff(new PauseWhenTakenOff(false));
assertRequest(requestDisabled, "3e:0c:00:00:00:00:03:f8:01:01:09:3c");
assertRequests(protocol::setPauseWhenTakenOff, new LinkedHashMap<PauseWhenTakenOff, String>() {{
put(new PauseWhenTakenOff(false), "f8:01:01");
put(new PauseWhenTakenOff(true), "f8:01:00");
}});
}
@Test
@ -164,7 +261,14 @@ public class SonyProtocolImplV2Test {
@Test
public void setEqualizerCustomBands() {
// TODO
assertRequests(protocol::setEqualizerCustomBands, new LinkedHashMap<EqualizerCustomBands, String>() {{
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 1), 0), "58:00:a0:06:0a:0a:0b:0c:0d:0b");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), 0), "58:00:a0:06:0a:0a:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 4, 5), 0), "58:00:a0:06:0a:0a:0b:0c:0e:0f");
put(new EqualizerCustomBands(Arrays.asList(5, 1, 2, 3, 5), 0), "58:00:a0:06:0a:0f:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), -6), "58:00:a0:06:04:0a:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), 10), "58:00:a0:06:14:0a:0b:0c:0d:0f");
}});
}
@Test
@ -203,8 +307,11 @@ public class SonyProtocolImplV2Test {
}
@Test
@Ignore("Not implemented on V2")
public void setVoiceNotifications() {
assertRequests(protocol::setVoiceNotifications, 0x0e, new LinkedHashMap<VoiceNotifications, String>() {{
put(new VoiceNotifications(false), "48:01:01");
put(new VoiceNotifications(true), "48:01:00");
}});
}
@Test
@ -213,8 +320,9 @@ public class SonyProtocolImplV2Test {
}
@Test
@Ignore("Not implemented on V2")
public void powerOff() {
final Request request = protocol.powerOff();
assertRequest(request, 0x0c, "24:03:01");
}
@Test
@ -371,4 +479,47 @@ public class SonyProtocolImplV2Test {
@Ignore("Not implemented on V2")
public void handleVoiceNotifications() {
}
@Test
public void handleQuickAccess() {
final Map<String, QuickAccess> commands = new LinkedHashMap<String, QuickAccess>() {{
// Ret
put("3e:0c:00:00:00:00:05:f7:0d:02:00:00:17:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF));
put("3e:0c:01:00:00:00:05:f7:0d:02:00:01:19:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY));
put("3e:0c:01:00:00:00:05:f7:0d:02:01:00:19:3c", new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF));
// Notify
put("3e:0c:00:00:00:00:05:f9:0d:02:00:00:19:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF));
put("3e:0c:01:00:00:00:05:f9:0d:02:00:01:1b:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY));
put("3e:0c:01:00:00:00:05:f9:0d:02:01:00:1b:3c", new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF));
}};
for (Map.Entry<String, QuickAccess> entry : commands.entrySet()) {
final List<? extends GBDeviceEvent> events = handleMessage(protocol, entry.getKey());
assertPrefs(events, entry.getValue().toPreferences());
}
}
@Test
public void handleAmbientSoundControlButtonMode() {
final Map<AmbientSoundControlButtonMode, String> commands = new LinkedHashMap<AmbientSoundControlButtonMode, String>() {{
// Notify
put(AmbientSoundControlButtonMode.NC_AS_OFF, "3e:0c:01:00:00:00:07:fd:03:01:35:01:00:01:4c:3c");
put(AmbientSoundControlButtonMode.NC_AS, "3e:0c:00:00:00:00:07:fd:03:01:35:01:00:02:4c:3c");
put(AmbientSoundControlButtonMode.NC_OFF, "3e:0c:00:00:00:00:07:fd:03:01:35:01:00:03:4d:3c");
put(AmbientSoundControlButtonMode.AS_OFF, "3e:0c:01:00:00:00:07:fd:03:01:35:01:00:04:4f:3c");
}};
for (Map.Entry<AmbientSoundControlButtonMode, String> entry : commands.entrySet()) {
final List<? extends GBDeviceEvent> events = handleMessage(protocol, entry.getValue());
assertEquals("Expect 1 events", 1, events.size());
final GBDeviceEventUpdatePreferences event = (GBDeviceEventUpdatePreferences) events.get(0);
final Map<String, Object> expectedPrefs = entry.getKey().toPreferences();
assertEquals("Expect 1 prefs", 1, expectedPrefs.size());
final Object modePrefValue = expectedPrefs
.get(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE);
assertNotNull(modePrefValue);
assertEquals(modePrefValue, event.preferences.get(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE));
}
}
}

View File

@ -1,241 +0,0 @@
/* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v3;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertPrefs;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequest;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequests;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.handleMessage;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AutomaticPowerOff;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.PauseWhenTakenOff;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.QuickAccess;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatEnabled;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.MockSonyCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType;
public class SonyProtocolImplV3Test {
private final MockSonyCoordinator coordinator = new MockSonyCoordinator();
private final SonyProtocolImplV3 protocol = new SonyProtocolImplV3(null) {
@Override
protected SonyHeadphonesCoordinator getCoordinator() {
return coordinator;
}
};
@Before
public void before() {
coordinator.getCapabilities().clear();
}
@Test
public void setAmbientSoundControl() {
final Map<AmbientSoundControl, String> commands = new LinkedHashMap<AmbientSoundControl, String>() {{
put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, false, 20), "68:17:01:01:01:00:14");
put(new AmbientSoundControl(AmbientSoundControl.Mode.OFF, false, 20), "68:17:01:00:00:00:14");
put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, false, 10), "68:17:01:01:01:00:0a");
put(new AmbientSoundControl(AmbientSoundControl.Mode.AMBIENT_SOUND, true, 20), "68:17:01:01:01:01:14");
put(new AmbientSoundControl(AmbientSoundControl.Mode.NOISE_CANCELLING, false, 20), "68:17:01:01:00:00:14");
}};
for (Map.Entry<AmbientSoundControl, String> entry : commands.entrySet()) {
final Request request = protocol.setAmbientSoundControl(entry.getKey());
assertRequest(request, 0x0c, entry.getValue());
}
}
@Test
public void setSpeakToChatEnabled() {
assertRequests(protocol::setSpeakToChatEnabled, new LinkedHashMap<SpeakToChatEnabled, String>() {{
put(new SpeakToChatEnabled(false), "f8:0c:01:01");
put(new SpeakToChatEnabled(true), "f8:0c:00:01");
}});
}
@Test
public void setSpeakToChatConfig() {
assertRequests(protocol::setSpeakToChatConfig, new LinkedHashMap<SpeakToChatConfig, String>() {{
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.HIGH, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:01:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.LOW, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:02:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:00:01");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.SHORT), "fc:0c:00:00");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.LONG), "fc:0c:00:02");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.OFF), "fc:0c:00:03");
put(new SpeakToChatConfig(false, SpeakToChatConfig.Sensitivity.AUTO, SpeakToChatConfig.Timeout.STANDARD), "fc:0c:00:01");
}});
}
@Test
public void getBattery() {
final Map<BatteryType, String> commands = new LinkedHashMap<BatteryType, String>() {{
put(BatteryType.SINGLE, "22:00");
put(BatteryType.DUAL, "22:09");
put(BatteryType.CASE, "22:0a");
}};
for (Map.Entry<BatteryType, String> entry : commands.entrySet()) {
final Request request = protocol.getBattery(entry.getKey());
assertRequest(request, 0x0c, entry.getValue());
}
}
@Test
public void getQuickAccess() {
final Request request = protocol.getQuickAccess();
assertRequest(request, "3e:0c:00:00:00:00:02:f6:0d:11:3c");
}
@Test
public void setQuickAccess() {
final Map<QuickAccess, String> commands = new LinkedHashMap<QuickAccess, String>() {{
put(new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF), "3e:0c:01:00:00:00:05:f8:0d:02:00:00:19:3c");
put(new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY), "3e:0c:00:00:00:00:05:f8:0d:02:00:01:19:3c");
put(new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF), "3e:0c:00:00:00:00:05:f8:0d:02:01:00:19:3c");
}};
for (Map.Entry<QuickAccess, String> entry : commands.entrySet()) {
final Request request = protocol.setQuickAccess(entry.getKey());
assertRequest(request, entry.getValue());
}
}
@Test
public void getAmbientSoundControlButtonMode() {
final Request request = protocol.getAmbientSoundControlButtonMode();
assertRequest(request, "3e:0c:00:00:00:00:02:fa:03:0b:3c");
}
@Test
public void setAmbientSoundControlButtonMode() {
final Map<AmbientSoundControlButtonMode, String> commands = new LinkedHashMap<AmbientSoundControlButtonMode, String>() {{
put(AmbientSoundControlButtonMode.NC_AS_OFF, "3e:0c:00:00:00:00:07:fc:03:01:35:01:00:01:4a:3c");
put(AmbientSoundControlButtonMode.NC_AS, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:02:4c:3c");
put(AmbientSoundControlButtonMode.NC_OFF, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:03:4d:3c");
put(AmbientSoundControlButtonMode.AS_OFF, "3e:0c:01:00:00:00:07:fc:03:01:35:01:00:04:4e:3c");
}};
for (Map.Entry<AmbientSoundControlButtonMode, String> entry : commands.entrySet()) {
final Request request = protocol.setAmbientSoundControlButtonMode(entry.getKey());
assertRequest(request, entry.getValue());
}
}
@Test
public void setPauseWhenTakenOff() {
assertRequests(protocol::setPauseWhenTakenOff, new LinkedHashMap<PauseWhenTakenOff, String>() {{
put(new PauseWhenTakenOff(false), "f8:01:01");
put(new PauseWhenTakenOff(true), "f8:01:00");
}});
}
@Test
public void setEqualizerCustomBands() {
assertRequests(protocol::setEqualizerCustomBands, new LinkedHashMap<EqualizerCustomBands, String>() {{
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 1), 0), "58:00:a0:06:0a:0a:0b:0c:0d:0b");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), 0), "58:00:a0:06:0a:0a:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 4, 5), 0), "58:00:a0:06:0a:0a:0b:0c:0e:0f");
put(new EqualizerCustomBands(Arrays.asList(5, 1, 2, 3, 5), 0), "58:00:a0:06:0a:0f:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), -6), "58:00:a0:06:04:0a:0b:0c:0d:0f");
put(new EqualizerCustomBands(Arrays.asList(0, 1, 2, 3, 5), 10), "58:00:a0:06:14:0a:0b:0c:0d:0f");
}});
}
@Test
public void setAutomaticPowerOff() {
assertRequests(protocol::setAutomaticPowerOff, new LinkedHashMap<AutomaticPowerOff, String>() {{
put(AutomaticPowerOff.OFF, "28:05:11:00");
put(AutomaticPowerOff.WHEN_TAKEN_OFF, "28:05:10:00");
}});
}
@Test
public void setVoiceNotifications() {
assertRequests(protocol::setVoiceNotifications, 0x0e, new LinkedHashMap<VoiceNotifications, String>() {{
put(new VoiceNotifications(false), "48:01:01");
put(new VoiceNotifications(true), "48:01:00");
}});
}
@Test
public void powerOff() {
final Request request = protocol.powerOff();
assertRequest(request, 0x0c, "24:03:01");
}
@Test
public void handleQuickAccess() {
final Map<String, QuickAccess> commands = new LinkedHashMap<String, QuickAccess>() {{
// Ret
put("3e:0c:00:00:00:00:05:f7:0d:02:00:00:17:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF));
put("3e:0c:01:00:00:00:05:f7:0d:02:00:01:19:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY));
put("3e:0c:01:00:00:00:05:f7:0d:02:01:00:19:3c", new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF));
// Notify
put("3e:0c:00:00:00:00:05:f9:0d:02:00:00:19:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.OFF));
put("3e:0c:01:00:00:00:05:f9:0d:02:00:01:1b:3c", new QuickAccess(QuickAccess.Mode.OFF, QuickAccess.Mode.SPOTIFY));
put("3e:0c:01:00:00:00:05:f9:0d:02:01:00:1b:3c", new QuickAccess(QuickAccess.Mode.SPOTIFY, QuickAccess.Mode.OFF));
}};
for (Map.Entry<String, QuickAccess> entry : commands.entrySet()) {
final List<? extends GBDeviceEvent> events = handleMessage(protocol, entry.getKey());
assertPrefs(events, entry.getValue().toPreferences());
}
}
@Test
public void handleAmbientSoundControlButtonMode() {
final Map<AmbientSoundControlButtonMode, String> commands = new LinkedHashMap<AmbientSoundControlButtonMode, String>() {{
// Notify
put(AmbientSoundControlButtonMode.NC_AS_OFF, "3e:0c:01:00:00:00:07:fd:03:01:35:01:00:01:4c:3c");
put(AmbientSoundControlButtonMode.NC_AS, "3e:0c:00:00:00:00:07:fd:03:01:35:01:00:02:4c:3c");
put(AmbientSoundControlButtonMode.NC_OFF, "3e:0c:00:00:00:00:07:fd:03:01:35:01:00:03:4d:3c");
put(AmbientSoundControlButtonMode.AS_OFF, "3e:0c:01:00:00:00:07:fd:03:01:35:01:00:04:4f:3c");
}};
for (Map.Entry<AmbientSoundControlButtonMode, String> entry : commands.entrySet()) {
final List<? extends GBDeviceEvent> events = handleMessage(protocol, entry.getValue());
assertEquals("Expect 1 events", 1, events.size());
final GBDeviceEventUpdatePreferences event = (GBDeviceEventUpdatePreferences) events.get(0);
final Map<String, Object> expectedPrefs = entry.getKey().toPreferences();
assertEquals("Expect 1 prefs", 1, expectedPrefs.size());
final Object modePrefValue = expectedPrefs
.get(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE);
assertNotNull(modePrefValue);
assertEquals(modePrefValue, event.preferences.get(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE));
}
}
}