1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-18 11:00:09 +02:00

Sony WH-1000XM5: Initial support

This commit is contained in:
José Rebelo 2023-05-06 16:03:48 +01:00
parent e4933a0b42
commit 7b3fbeb4af
10 changed files with 293 additions and 11 deletions

View File

@ -3,6 +3,7 @@
#### Next Version
* Initial support for Amazfit GTR 3 Pro
* Initial support for Sony WH-1000XM5
* Amazfit Bip U: Remove alarm snooze option
* Amazfit GTR 4 / GTS 4: Add watch Wi-Fi Hotspot and FTP Server
* Amazfit GTR 4 / GTS 4: Perform and receive phone calls on watch

View File

@ -89,7 +89,7 @@ vendor's servers.
- [SMA](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/SMA) Q2 (SMA-Q2-OSS Firmware)
- [Sony Headphones](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Sony-Headphones)
- LinkBuds S
- WH-1000XM2, WH-1000XM3, WH-1000XM4
- WH-1000XM2, WH-1000XM3, WH-1000XM4, WH-1000XM5
- WF-SP800N
- WF-1000XM3, WF-1000XM4
- Teclast H10, H30

View File

@ -0,0 +1,65 @@
/* Copyright (C) 2023 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.devices.sony.headphones.coordinators;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
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.DeviceType;
public class SonyWH1000XM5Coordinator extends SonyHeadphonesCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(final GBDeviceCandidate candidate) {
if (candidate.getName().contains("WH-1000XM5")) {
return DeviceType.SONY_WH_1000XM5;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.SONY_WH_1000XM5;
}
@Override
public List<SonyHeadphonesCapabilities> getCapabilities() {
return Arrays.asList(
// TODO R.xml.devicesettings_connect_two_devices,
// TODO automatic ANC depending on state (might need phone?)
SonyHeadphonesCapabilities.BatterySingle,
// TODO SonyHeadphonesCapabilities.PowerOffFromPhone,
SonyHeadphonesCapabilities.AmbientSoundControl,
SonyHeadphonesCapabilities.SpeakToChatEnabled,
SonyHeadphonesCapabilities.SpeakToChatConfig,
// TODO SonyHeadphonesCapabilities.AudioUpsampling,
// TODO SonyHeadphonesCapabilities.AmbientSoundControlButtonMode,
SonyHeadphonesCapabilities.VoiceNotifications,
SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff,
// TODO SonyHeadphonesCapabilities.TouchSensorSingle,
SonyHeadphonesCapabilities.EqualizerWithCustomBands,
SonyHeadphonesCapabilities.QuickAccess,
SonyHeadphonesCapabilities.PauseWhenTakenOff
);
}
}

View File

@ -124,6 +124,7 @@ public enum DeviceType {
SONY_WH_1000XM2(434, R.drawable.ic_device_sony_overhead, R.drawable.ic_device_sony_overhead_disabled, R.string.devicetype_sony_wh_1000xm2),
SONY_WF_1000XM4(435, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_wf_1000xm4),
SONY_LINKBUDS_S(436, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_linkbuds_s),
SONY_WH_1000XM5(437, R.drawable.ic_device_sony_overhead, R.drawable.ic_device_sony_overhead_disabled, R.string.devicetype_sony_wh_1000xm5),
BOSE_QC35(440, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_bose_qc35),
VESC_NRF(500, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc),
VESC_HM10(501, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc),

View File

@ -353,6 +353,8 @@ public class DeviceSupportFactory {
return new ServiceDeviceSupport(new SonyHeadphonesSupport());
case SONY_LINKBUDS_S:
return new ServiceDeviceSupport(new SonyHeadphonesSupport());
case SONY_WH_1000XM5:
return new ServiceDeviceSupport(new SonyHeadphonesSupport());
case VESC_NRF:
case VESC_HM10:
return new ServiceDeviceSupport(new VescDeviceSupport(device.getType()));

View File

@ -117,8 +117,11 @@ public class SonyHeadphonesProtocol extends GBDeviceProtocol {
break;
case 0x03:
// LinkBuds S 2.0.2: 01:00:03:00:00:07:00:00
default:
// WH-1000XM5 1.1.3: 01:00:03:00:00:00:00:00
protocolImpl = new SonyProtocolImplV3(getDevice());
break;
default:
LOG.error("Unexpected version for payload of length 8: {}", message.getPayload()[2]);
return null;
}
} else {

View File

@ -21,24 +21,22 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
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.EqualizerPreset;
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.v1.params.BatteryType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2.PayloadTypeV2;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2.SonyProtocolImplV2;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -62,7 +60,7 @@ public class SonyProtocolImplV3 extends SonyProtocolImplV2 {
@Override
public Request setAmbientSoundControl(final AmbientSoundControl ambientSoundControl) {
final ByteBuffer buf = ByteBuffer.allocate(8);
final ByteBuffer buf = ByteBuffer.allocate(7);
buf.put(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getCode());
buf.put((byte) 0x17);
@ -86,6 +84,54 @@ public class SonyProtocolImplV3 extends SonyProtocolImplV2 {
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(
@ -138,6 +184,49 @@ public class SonyProtocolImplV3 extends SonyProtocolImplV2 {
);
}
@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]);
@ -261,6 +350,35 @@ public class SonyProtocolImplV3 extends SonyProtocolImplV2 {
return Collections.singletonList(event);
}
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) {

View File

@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgts4mini.Amazfi
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfittrex2.AmazfitTRex2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyLinkBudsSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soflow.SoFlowCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
@ -363,6 +364,7 @@ public class DeviceHelper {
result.add(new SonyWF1000XM3Coordinator());
result.add(new SonyWH1000XM2Coordinator());
result.add(new SonyWF1000XM4Coordinator());
result.add(new SonyWH1000XM5Coordinator());
result.add(new QC35Coordinator());
result.add(new BinarySensorCoordinator());
result.add(new FlipperZeroCoordinator());

View File

@ -1276,6 +1276,7 @@
<string name="devicetype_sony_wh_1000xm2">Sony WH-1000XM2</string>
<string name="devicetype_sony_wh_1000xm3">Sony WH-1000XM3</string>
<string name="devicetype_sony_wh_1000xm4">Sony WH-1000XM4</string>
<string name="devicetype_sony_wh_1000xm5">Sony WH-1000XM5</string>
<string name="devicetype_sony_wf_sp800n">Sony WF-SP800N</string>
<string name="devicetype_sony_wf_1000xm3">Sony WF-1000XM3</string>
<string name="devicetype_sony_wf_1000xm4">Sony WF-1000XM4</string>

View File

@ -20,10 +20,13 @@ 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;
@ -32,19 +35,69 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
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.coordinators.SonyLinkBudsSCoordinator;
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;
public class SonyProtocolImplV3Test {
private final MockSonyCoordinator coordinator = new MockSonyCoordinator();
private final SonyProtocolImplV3 protocol = new SonyProtocolImplV3(null) {
@Override
protected SonyHeadphonesCoordinator getCoordinator() {
return new SonyLinkBudsSCoordinator();
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 getQuickAccess() {
final Request request = protocol.getQuickAccess();
@ -86,6 +139,42 @@ public class SonyProtocolImplV3Test {
}
}
@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 handleQuickAccess() {
final Map<String, QuickAccess> commands = new LinkedHashMap<String, QuickAccess>() {{