Merge pull request 'Moondrop: add support for Moondrop Space Travel 2 and its audio curation modes (ANC)' (#6062)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6062
This commit is contained in:
José Rebelo
2026-05-09 23:57:37 +02:00
11 changed files with 323 additions and 8 deletions
@@ -611,6 +611,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_MOONDROP_TOUCH_ASSISTANT_TRIGGER = "pref_moondrop_touch_assistant_trigger";
public static final String PREF_MOONDROP_TOUCH_ANC_MODE_EARBUD = "pref_moondrop_touch_anc_mode_earbud";
public static final String PREF_MOONDROP_TOUCH_ANC_MODE_TRIGGER = "pref_moondrop_touch_anc_mode_trigger";
public static final String PREF_MOONDROP_ANC_MODE = "pref_moondrop_anc_mode";
public static final String PREF_MISCALE_SMALL_OBJECTS = "pref_miscale_small_objects";
@@ -928,6 +928,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE);
addPreferenceHandlerFor(PREF_MOONDROP_EQUALIZER_PRESET);
addPreferenceHandlerFor(PREF_MOONDROP_ANC_MODE);
addPreferenceHandlerFor(PREF_MOONDROP_TOUCH_PLAY_PAUSE_EARBUD);
addPreferenceHandlerFor(PREF_MOONDROP_TOUCH_PLAY_PAUSE_TRIGGER);
addPreferenceHandlerFor(PREF_MOONDROP_TOUCH_MEDIA_PREV_EARBUD);
@@ -0,0 +1,69 @@
/* Copyright (C) 2026 Jan Petrlík
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moondrop;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.moondrop.MoondropSpaceTravel2DeviceSupport;
public class MoondropSpaceTravel2Coordinator extends MoondropSpaceTravelCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Space Travel 2");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_moondrop_space_travel_2;
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
settings.addRootScreen(R.xml.devicesettings_moondrop_space_travel_2_audio);
settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
settings.addSubScreen(
DeviceSpecificSettingsScreen.TOUCH_OPTIONS,
R.xml.devicesettings_moondrop_space_travel_touch);
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
settings.addSubScreen(
DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS,
R.xml.devicesettings_headphones);
return settings;
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new MoondropSpaceTravel2SettingsCustomizer(device);
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
return MoondropSpaceTravel2DeviceSupport.class;
}
}
@@ -0,0 +1,83 @@
/* Copyright (C) 2026 Jan Petrlík
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moondrop;
import android.os.Parcel;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_MOONDROP_ANC_MODE;
public class MoondropSpaceTravel2SettingsCustomizer implements DeviceSpecificSettingsCustomizer {
private final GBDevice device;
public MoondropSpaceTravel2SettingsCustomizer(final GBDevice device) {
this.device = device;
}
@Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
}
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs,
final String rootKey) {
if (rootKey == null) {
// The device does not emit notifications when the user changes ANC mode via
// touch, so refresh the current mode whenever the user opens the settings screen.
GBApplication.deviceService(device).onReadConfiguration(PREF_MOONDROP_ANC_MODE);
}
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
public static final Creator<MoondropSpaceTravel2SettingsCustomizer> CREATOR = new Creator<MoondropSpaceTravel2SettingsCustomizer>() {
@Override
public MoondropSpaceTravel2SettingsCustomizer createFromParcel(final Parcel in) {
final GBDevice device = in.readParcelable(MoondropSpaceTravel2SettingsCustomizer.class.getClassLoader());
return new MoondropSpaceTravel2SettingsCustomizer(device);
}
@Override
public MoondropSpaceTravel2SettingsCustomizer[] newArray(final int size) {
return new MoondropSpaceTravel2SettingsCustomizer[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(device, 0);
}
}
@@ -332,6 +332,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coor
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaXmwsdj04Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiCompositionScaleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiSmartScaleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moondrop.MoondropSpaceTravel2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moondrop.MoondropSpaceTravelCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.AdvanWatchSE1AICoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.BlackviewR60Coordinator;
@@ -805,6 +806,7 @@ public enum DeviceType {
SOUNDCORE_Q30(SoundcoreQ30Coordinator.class),
SOUNDCORE_AEROFIT2(SoundcoreAeroFit2Coordinator.class),
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
MOONDROP_SPACE_TRAVEL_2(MoondropSpaceTravel2Coordinator.class),
BOSE_QC35(QC35Coordinator.class),
ONEMORE_SONOFLOW(OneMoreSonoFlowCoordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
@@ -0,0 +1,49 @@
/* Copyright (C) 2026 Jan Petrlík
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moondrop;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_MOONDROP_ANC_MODE;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractHeadphoneSerialDeviceSupportV2;
public class MoondropSpaceTravel2DeviceSupport extends AbstractHeadphoneSerialDeviceSupportV2<MoondropSpaceTravel2Protocol> {
@Override
protected MoondropSpaceTravel2Protocol createDeviceProtocol() {
return new MoondropSpaceTravel2Protocol(getDevice());
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
builder.write(mDeviceProtocol.encodeGetEqualizerPreset());
builder.write(mDeviceProtocol.encodeGetTouchActions());
builder.write(mDeviceProtocol.encodeGetAudioCurationMode());
builder.setDeviceState(GBDevice.State.INITIALIZED);
return builder;
}
@Override
public void onReadConfiguration(final String config) {
if (PREF_MOONDROP_ANC_MODE.equals(config)) {
final TransactionBuilder builder = createTransactionBuilder("read anc mode");
builder.write(mDeviceProtocol.encodeGetAudioCurationMode());
builder.queue();
}
}
}
@@ -0,0 +1,76 @@
/* Copyright (C) 2026 Jan Petrlík
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moondrop;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_MOONDROP_ANC_MODE;
public class MoondropSpaceTravel2Protocol extends MoondropSpaceTravelProtocol {
private static final byte AUDIO_CURATION_FEATURE = (byte)0x08;
private static final byte AUDIO_CURATION_PDU_GET_MODE = (byte)0x03;
private static final byte AUDIO_CURATION_PDU_SET_MODE = (byte)0x04;
protected MoondropSpaceTravel2Protocol(GBDevice device) {
super(device);
}
@Override
protected GBDeviceEvent decodePacket(short featureId, short pduId, byte[] payload) {
if (featureId == AUDIO_CURATION_FEATURE && pduId == AUDIO_CURATION_PDU_GET_MODE)
return handlePacketAudioCurationMode(payload);
return super.decodePacket(featureId, pduId, payload);
}
@Override
public byte[] encodeSendConfiguration(String config) {
if (PREF_MOONDROP_ANC_MODE.equals(config))
return encodeSetAudioCurationMode();
return super.encodeSendConfiguration(config);
}
public byte[] encodeGetAudioCurationMode() {
return new GaiaPacket(AUDIO_CURATION_FEATURE, AUDIO_CURATION_PDU_GET_MODE).encode();
}
private byte[] encodeSetAudioCurationMode() {
Prefs prefs = getDevicePrefs();
byte mode = Byte.parseByte(prefs.getString(PREF_MOONDROP_ANC_MODE, "1"));
byte[] payload = new byte[] { mode };
return new GaiaPacket(AUDIO_CURATION_FEATURE, AUDIO_CURATION_PDU_SET_MODE, payload).encode();
}
private GBDeviceEvent handlePacketAudioCurationMode(byte[] payload) {
if (payload.length < 1)
return null;
// GET_MODE returns a 0-based slot index, SET_MODE and the UI dropdown use a
// bitmask (Normal=1, ANC=2, Transparency=4). Convert before storing the
// preference.
int slot = payload[0] & 0xff;
if (slot > 2)
return null;
int mode = 1 << slot;
return new GBDeviceEventUpdatePreferences(PREF_MOONDROP_ANC_MODE, String.valueOf(mode));
}
}
@@ -59,19 +59,22 @@ public class MoondropSpaceTravelProtocol extends GBDeviceProtocol {
if (packet.getPduType() != GaiaPacket.PDU_RESPONSE)
continue;
short featureId = packet.getFeatureId();
short pduId = packet.getPduId();
byte[] payload = packet.getPayload();
if (featureId == EQUALIZER_PRESET_FEATURE && pduId == EQUALIZER_PRESET_PDU_GET)
events.add(handlePacketEqualizerPreset(payload));
else if (featureId == TOUCH_ACTIONS_FEATURE && pduId == TOUCH_ACTIONS_PDU_GET)
events.add(handlePacketTouchActions(payload));
GBDeviceEvent event = decodePacket(packet.getFeatureId(), packet.getPduId(), packet.getPayload());
if (event != null)
events.add(event);
}
return events.toArray(new GBDeviceEvent[0]);
}
protected GBDeviceEvent decodePacket(short featureId, short pduId, byte[] payload) {
if (featureId == EQUALIZER_PRESET_FEATURE && pduId == EQUALIZER_PRESET_PDU_GET)
return handlePacketEqualizerPreset(payload);
else if (featureId == TOUCH_ACTIONS_FEATURE && pduId == TOUCH_ACTIONS_PDU_GET)
return handlePacketTouchActions(payload);
return null;
}
private GBDeviceEvent handlePacketEqualizerPreset(byte[] payload) {
if (payload.length != EQUALIZER_PRESET_PKT_LEN)
return null;
+12
View File
@@ -4773,6 +4773,18 @@
<item>2</item>
</string-array>
<string-array name="moondrop_anc_mode_names">
<item>@string/prefs_active_noise_cancelling_level_normal</item>
<item>@string/prefs_active_noise_cancelling</item>
<item>@string/prefs_active_noise_cancelling_transparency</item>
</string-array>
<string-array name="moondrop_anc_mode_values">
<item>1</item>
<item>2</item>
<item>4</item>
</string-array>
<string-array name="moondrop_touch_earbud_names">
<item>@string/left</item>
<item>@string/right</item>
+1
View File
@@ -2143,6 +2143,7 @@
<string name="devicetype_soundcore_aerofit2" translatable="false">Soundcore AeroFit 2</string>
<string name="devicetype_pixel_buds_a_series" translatable="false">Pixel Buds A-Series</string>
<string name="devicetype_moondrop_space_travel" translatable="false">Moondrop Space Travel</string>
<string name="devicetype_moondrop_space_travel_2" translatable="false">Moondrop Space Travel 2</string>
<string name="devicetype_binary_sensor">Binary sensor</string>
<string name="devicetype_honor_band3" translatable="false">Honor Band 3</string>
<string name="devicetype_honor_band4" translatable="false">Honor Band 4</string>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:entries="@array/moondrop_equalizer_preset_names"
android:entryValues="@array/moondrop_equalizer_preset_values"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_moondrop_equalizer_preset"
app:useSimpleSummaryProvider="true"
android:title="@string/prefs_equalizer_preset" />
<ListPreference
android:entries="@array/moondrop_anc_mode_names"
android:entryValues="@array/moondrop_anc_mode_values"
android:icon="@drawable/ic_voice"
android:key="pref_moondrop_anc_mode"
app:useSimpleSummaryProvider="true"
android:title="@string/prefs_active_noise_cancelling_level" />
</androidx.preference.PreferenceScreen>