diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java
index da2f49302..965c358b7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java
@@ -372,6 +372,33 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT = "pref_soundcore_control_double_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT = "pref_soundcore_control_triple_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT = "pref_soundcore_control_long_press_action_right";
+ public static final String PREF_SOUNDCORE_VOICE_PROMPTS = "pref_soundcore_voice_prompts";
+ public static final String PREF_SOUNDCORE_BUTTON_BRIGHTNESS = "pref_soundcore_button_brightness";
+ public static final String PREF_SOUNDCORE_AUTO_POWER_OFF = "pref_soundcore_auto_power_off";
+ public static final String PREF_SOUNDCORE_LDAC_MODE = "pref_soundcore_ldac_mode";
+ public static final String PREF_SOUNDCORE_ADAPTIVE_DIRECTION = "pref_soundcore_adaptive_direction";
+ public static final String PREF_SOUNDCORE_EQUALIZER_PRESET = "pref_soundcore_equalizer_preset";
+ public static final String PREF_SOUNDCORE_EQUALIZER_CUSTOM = "pref_soundcore_equalizer_custom";
+ public static final String PREF_SOUNDCORE_EQUALIZER_DIRECTION = "pref_soundcore_equalizer_direction";
+ public static final String PREF_SOUNDCORE_EQUALIZER_RESET = "pref_soundcore_equalizer_reset";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ = "pref_soundcore_equalizer_band1_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE = "pref_soundcore_equalizer_band1_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ = "pref_soundcore_equalizer_band2_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE = "pref_soundcore_equalizer_band2_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ = "pref_soundcore_equalizer_band3_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE = "pref_soundcore_equalizer_band3_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ = "pref_soundcore_equalizer_band4_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE = "pref_soundcore_equalizer_band4_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ = "pref_soundcore_equalizer_band5_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE = "pref_soundcore_equalizer_band5_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ = "pref_soundcore_equalizer_band6_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE = "pref_soundcore_equalizer_band6_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ = "pref_soundcore_equalizer_band7_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE = "pref_soundcore_equalizer_band7_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ = "pref_soundcore_equalizer_band8_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE = "pref_soundcore_equalizer_band8_value";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ = "pref_soundcore_equalizer_band9_freq";
+ public static final String PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE = "pref_soundcore_equalizer_band9_value";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE = "pref_sony_ambient_sound_control_button_mode";
public static final String PREF_SONY_FOCUS_VOICE = "pref_sony_focus_voice";
public static final String PREF_SONY_AMBIENT_SOUND_LEVEL = "pref_sony_ambient_sound_level";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
index 2b6810132..04a7523ef 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
@@ -715,6 +715,31 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_VOICE_PROMPTS);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_BUTTON_BRIGHTNESS);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_AUTO_POWER_OFF);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_LDAC_MODE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_DIRECTION);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_PRESET);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_DIRECTION);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ);
+ addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE);
addPreferenceHandlerFor(PREF_MOONDROP_EQUALIZER_PRESET);
addPreferenceHandlerFor(PREF_MOONDROP_TOUCH_PLAY_PAUSE_EARBUD);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java
new file mode 100644
index 000000000..1299e86d1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java
@@ -0,0 +1,93 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300;
+
+import androidx.annotation.NonNull;
+
+import java.util.regex.Pattern;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+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.devices.AbstractBLClassicDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300.SoundcoreMotion300DeviceSupport;
+
+public class SoundcoreMotion300Coordinator extends AbstractBLClassicDeviceCoordinator {
+ @Override
+ public String getManufacturer() {
+ return "Anker";
+ }
+
+ @Override
+ protected Pattern getSupportedDeviceName() {
+ return Pattern.compile("soundcore Motion 300");
+ }
+
+ @Override
+ public int getDeviceNameResource() {
+ return R.string.devicetype_soundcore_motion300;
+ }
+
+ @Override
+ public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
+ return new SoundcoreMotion300SettingsCustomizer();
+ }
+
+ @Override
+ public int getBatteryCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean supportsPowerOff() {
+ return true;
+ }
+
+ @Override
+ protected void deleteDevice(
+ @NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session)
+ throws GBException {}
+
+ @Override
+ public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
+ final DeviceSpecificSettings settings = new DeviceSpecificSettings();
+
+ settings.addRootScreen(R.xml.devicesettings_soundcore_motion300);
+ settings.addRootScreen(DeviceSpecificSettingsScreen.AUDIO);
+ settings.addSubScreen(
+ DeviceSpecificSettingsScreen.AUDIO,
+ R.xml.devicesettings_soundcore_motion300_audio);
+ settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
+ settings.addSubScreen(
+ DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS,
+ R.xml.devicesettings_headphones);
+
+ return settings;
+ }
+
+ @NonNull
+ @Override
+ public Class extends DeviceSupport> getDeviceSupportClass() {
+ return SoundcoreMotion300DeviceSupport.class;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java
new file mode 100644
index 000000000..251fae92c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java
@@ -0,0 +1,113 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300;
+
+import android.os.Parcel;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.SeekBarPreference;
+
+import java.util.Collections;
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300.SoundcoreMotion300Protocol;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
+
+public class SoundcoreMotion300SettingsCustomizer implements DeviceSpecificSettingsCustomizer {
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public SoundcoreMotion300SettingsCustomizer createFromParcel(final Parcel in) {
+ return new SoundcoreMotion300SettingsCustomizer();
+ }
+
+ @Override
+ public SoundcoreMotion300SettingsCustomizer[] newArray(final int size) {
+ return new SoundcoreMotion300SettingsCustomizer[size];
+ }
+ };
+
+ @Override
+ public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
+ if (!preference.getKey().equals(PREF_SOUNDCORE_EQUALIZER_PRESET))
+ return;
+
+ String preset = ((ListPreference)preference).getEntry().toString();
+ Preference pref = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_CUSTOM);
+ boolean customEnabled = preset.equals(handler.getContext().getString(R.string.custom));
+
+ if (pref != null)
+ pref.setEnabled(customEnabled);
+ }
+
+ @Override
+ public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
+ ListPreference equalizerDirection = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_DIRECTION);
+
+ if (equalizerDirection != null) {
+ boolean enabled = prefs.getBoolean(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, true);
+
+ equalizerDirection.setVisible(enabled);
+ }
+
+ Preference equalizerReset = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_RESET);
+
+ if (equalizerReset != null)
+ equalizerReset.setOnPreferenceClickListener(pref -> resetEqualizer(handler));
+ }
+
+ private boolean resetEqualizer(final DeviceSpecificSettingsHandler handler) {
+ // Reset all bands to default settings
+ for (int i = 0; i < SoundcoreMotion300Protocol.EQUALIZER_PREFS_FREQ.length; i++) {
+ ListPreference prefFreq = handler.findPreference(SoundcoreMotion300Protocol.EQUALIZER_PREFS_FREQ[i]);
+ SeekBarPreference prefValue = handler.findPreference(SoundcoreMotion300Protocol.EQUALIZER_PREFS_VALUE[i]);
+
+ // Neutral configuration
+ prefValue.setValue(60);
+
+ // Default configuration: 80 Hz, 150 Hz, 300 Hz, 600 Hz, 1.2 kHz, 2.5 kHz, 5 kHz, 9 kHz, 13 kHz
+ if (i < 7)
+ prefFreq.setValue("7");
+ else if (i == 7)
+ prefFreq.setValue("6");
+ else if (i == 8)
+ prefFreq.setValue("1");
+ }
+
+ // Send updated equalizer configuration
+ handler.notifyPreferenceChanged(PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ);
+
+ return true;
+ }
+
+ @Override
+ public Set getPreferenceKeysWithSummary() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index ba21eefa4..6c858eb46 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -217,6 +217,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
@@ -436,6 +437,7 @@ public enum DeviceType {
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
+ SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java
new file mode 100644
index 000000000..22c5089e3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java
@@ -0,0 +1,112 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
+
+public class SoundcorePacket {
+ private static final Logger LOG = LoggerFactory.getLogger(SoundcorePacket.class);
+
+ private static final int HEADER_LENGTH = 10;
+ private static final short START_OF_PACKET_HOST = (short)0xee08;
+ private static final short START_OF_PACKET_DEVICE = (short)0xff09;
+ private static final byte DIRECTION_HOST = (byte)0x00;
+ private static final byte DIRECTION_DEVICE = (byte)0x01;
+
+ private short command;
+ private byte[] payload;
+
+ public SoundcorePacket(short command) {
+ this(command, new byte[] {});
+ }
+
+ public SoundcorePacket(short command, byte[] payload) {
+ LOG.debug("Packet: command={}", String.format("0x%04x", command));
+
+ this.command = command;
+ this.payload = payload;
+ }
+
+ public short getCommand() {
+ return command;
+ }
+
+ public byte[] getPayload() {
+ return payload;
+ }
+
+ public static SoundcorePacket decode(ByteBuffer buf) {
+ if (buf.remaining() < HEADER_LENGTH)
+ return null;
+
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ if (buf.getShort() != START_OF_PACKET_DEVICE) {
+ LOG.error("Invalid start of packet: {}", hexdump(buf.array()));
+ return null;
+ }
+
+ // Skip two zero bytes
+ buf.getShort();
+
+ if (buf.get() != DIRECTION_DEVICE) {
+ LOG.error("Invalid direction: {}", hexdump(buf.array()));
+ return null;
+ }
+
+ short command = buf.getShort();
+ short length = buf.getShort();
+
+ if (length < HEADER_LENGTH) {
+ LOG.error("Invalid length: {}", hexdump(buf.array()));
+ return null;
+ }
+
+ // Skip checksum byte at the end
+ byte[] payload = new byte[length - HEADER_LENGTH];
+ buf.get(payload);
+
+ return new SoundcorePacket(command, payload);
+ }
+
+ public byte[] encode() {
+ ByteBuffer buf = ByteBuffer.allocate(HEADER_LENGTH + payload.length);
+
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.putShort(START_OF_PACKET_HOST);
+ buf.putShort((short)0x0000);
+ buf.put(DIRECTION_HOST);
+ buf.putShort(command);
+ buf.putShort((short)(HEADER_LENGTH + payload.length));
+ buf.put(payload);
+
+ byte checksum = 0;
+
+ for (byte val : buf.array())
+ checksum += val;
+
+ buf.put(checksum);
+
+ return buf.array();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java
new file mode 100644
index 000000000..487c14009
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java
@@ -0,0 +1,43 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300;
+
+import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+
+public class SoundcoreMotion300DeviceSupport extends AbstractHeadphoneDeviceSupport {
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ @Override
+ protected GBDeviceProtocol createDeviceProtocol() {
+ return new SoundcoreMotion300Protocol(getDevice());
+ }
+
+ @Override
+ protected GBDeviceIoThread createDeviceIOThread() {
+ return new SoundcoreMotion300IOThread(
+ getDevice(),
+ getContext(),
+ (SoundcoreMotion300Protocol)getDeviceProtocol(),
+ SoundcoreMotion300DeviceSupport.this,
+ getBluetoothAdapter());
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java
new file mode 100644
index 000000000..8c4894530
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java
@@ -0,0 +1,75 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.os.Handler;
+import android.os.ParcelUuid;
+import androidx.annotation.NonNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
+
+import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
+
+public class SoundcoreMotion300IOThread extends BtClassicIoThread {
+ private static final Logger LOG = LoggerFactory.getLogger(SoundcoreMotion300IOThread.class);
+
+ private final SoundcoreMotion300Protocol protocol;
+ private final Handler handler = new Handler();
+
+ public SoundcoreMotion300IOThread(
+ GBDevice device,
+ Context context,
+ SoundcoreMotion300Protocol protocol,
+ SoundcoreMotion300DeviceSupport support,
+ BluetoothAdapter adapter) {
+ super(device, context, protocol, support, adapter);
+ this.protocol = protocol;
+ }
+
+ @Override
+ protected void initialize() {
+ setUpdateState(GBDevice.State.INITIALIZING);
+
+ // Device requires a little delay to respond to commands
+ handler.postDelayed(() -> write(protocol.encodeGetDeviceInfo()), 500);
+ }
+
+ @NonNull
+ protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
+ return UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3135");
+ }
+
+ @Override
+ protected byte[] parseIncoming(InputStream stream) throws IOException {
+ byte[] buffer = new byte[1048576];
+ int bytes = stream.read(buffer);
+ LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
+
+ return Arrays.copyOf(buffer, bytes);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java
new file mode 100644
index 000000000..5f218ec01
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java
@@ -0,0 +1,385 @@
+/* Copyright (C) 2024 Severin von Wnuck-Lipinski
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket;
+import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
+
+public class SoundcoreMotion300Protocol extends GBDeviceProtocol {
+ private static final Logger LOG = LoggerFactory.getLogger(SoundcoreMotion300Protocol.class);
+
+ // Some of these commands are not used right now, they serve as documentation
+ private static final short CMD_GET_DEVICE_INFO = (short)0x0101;
+ private static final short CMD_GET_LDAC_MODE = (short)0x7f01;
+ private static final short CMD_GET_BUTTON_BRIGHTNESS = (short)0x9310;
+ private static final short CMD_GET_EQUALIZER = (short)0x8902;
+ private static final short CMD_GET_CURRENT_DIRECTION = (short)0x8c02;
+
+ private static final short CMD_SET_VOICE_PROMPTS = (short)0x9001;
+ private static final short CMD_SET_BUTTON_BRIGHTNESS = (short)0x9210;
+ private static final short CMD_SET_AUTO_POWER_OFF = (short)0x8601;
+ private static final short CMD_SET_LDAC_MODE = (short)0xff01;
+ private static final short CMD_SET_ADAPTIVE_DIRECTION = (short)0x8a02;
+ private static final short CMD_SET_EQUALIZER_PRESET = (short)0x8b02;
+ private static final short CMD_SET_EQUALIZER_CUSTOM = (short)0x8d02;
+
+ private static final short CMD_NOTIFY_BATTERY_INFO = (short)0x0301;
+ private static final short CMD_NOTIFY_CHARGING_INFO = (short)0x0401;
+ private static final short CMD_NOTIFY_VOLUME_INFO = (short)0x0901;
+ private static final short CMD_NOTIFY_PLAYBACK_INFO = (short)0x2101;
+ private static final short CMD_NOTIFY_BASS_MODE = (short)0x8e02;
+
+ private static final short CMD_POWER_OFF = (short)0x8901;
+
+ public static final String[] EQUALIZER_PREFS_FREQ = new String[] {
+ PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ,
+ PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ
+ };
+
+ public static final String[] EQUALIZER_PREFS_VALUE = new String[] {
+ PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE,
+ PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE
+ };
+
+ private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
+
+ protected SoundcoreMotion300Protocol(GBDevice device) {
+ super(device);
+ }
+
+ @Override
+ public GBDeviceEvent[] decodeResponse(byte[] data) {
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ SoundcorePacket packet = SoundcorePacket.decode(buf);
+
+ if (packet == null)
+ return null;
+
+ short cmd = packet.getCommand();
+ byte[] payload = packet.getPayload();
+
+ switch (cmd) {
+ case CMD_GET_DEVICE_INFO:
+ return handlePacketDeviceInfo(payload);
+ case CMD_GET_LDAC_MODE:
+ return handlePacketLdacMode(payload);
+ case CMD_GET_BUTTON_BRIGHTNESS:
+ return handlePacketButtonBrightness(payload);
+ case CMD_GET_EQUALIZER:
+ return handlePacketEqualizer(payload);
+ case CMD_SET_ADAPTIVE_DIRECTION:
+ return handlePacketAdaptiveDirection(payload);
+ case CMD_NOTIFY_BATTERY_INFO:
+ return handlePacketBatteryInfo(payload);
+ case CMD_NOTIFY_CHARGING_INFO:
+ return handlePacketChargingInfo(payload);
+ }
+
+ return null;
+ }
+
+ private GBDeviceEvent[] handlePacketDeviceInfo(byte[] payload) {
+ if (payload.length != 29)
+ return null;
+
+ ByteBuffer buf = ByteBuffer.wrap(payload);
+ byte volume = buf.get();
+ byte batteryLevel = buf.get();
+ byte batteryCharging = buf.get();
+ byte currentlyPlaying = buf.get();
+ byte voicePrompts = buf.get();
+ byte autoPowerOffEnabled = buf.get();
+ byte autoPowerOffDuration = buf.get();
+ byte[] firmwareBytes = new byte[5];
+ byte[] serialBytes = new byte[17];
+
+ buf.get(firmwareBytes);
+ buf.get(serialBytes);
+
+ String fwVersion = new String(firmwareBytes, StandardCharsets.UTF_8);
+ String serialNumber = new String(serialBytes, StandardCharsets.UTF_8);
+
+ GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
+ versionInfo.fwVersion = fwVersion;
+ batteryInfo.state = batteryCharging == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING;
+ batteryInfo.level = batteryLevel * 20;
+
+ LOG.debug(
+ "Device info: volume={}, battery={}, charging={}, playing={}, prompts={}, autoOff={}, autoOffDuration={}",
+ volume, batteryLevel, batteryCharging, currentlyPlaying, voicePrompts, autoPowerOffEnabled, autoPowerOffDuration
+ );
+
+ return new GBDeviceEvent[] {
+ versionInfo,
+ batteryInfo,
+ new GBDeviceEventUpdateDeviceInfo("SERIAL: ", serialNumber),
+ new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_VOICE_PROMPTS, voicePrompts != (byte)0x00),
+ new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_AUTO_POWER_OFF, String.valueOf(autoPowerOffDuration + 1)),
+ new GBDeviceEventSendBytes(encodeRequest(CMD_GET_LDAC_MODE)),
+ };
+ }
+
+ private GBDeviceEvent[] handlePacketLdacMode(byte[] payload) {
+ if (payload.length != 1)
+ return null;
+
+ return new GBDeviceEvent[] {
+ new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_LDAC_MODE, payload[0] != (byte)0x00),
+ new GBDeviceEventSendBytes(encodeRequest(CMD_GET_BUTTON_BRIGHTNESS)),
+ };
+ }
+
+ private GBDeviceEvent[] handlePacketButtonBrightness(byte[] payload) {
+ if (payload.length != 1)
+ return null;
+
+ return new GBDeviceEvent[] {
+ new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_BUTTON_BRIGHTNESS, String.valueOf(payload[0])),
+ new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)),
+ };
+ }
+
+ private GBDeviceEvent[] handlePacketEqualizer(byte[] payload) {
+ if (payload.length != 57)
+ return null;
+
+ // Get direction chosen in custom equalizer preferences
+ Prefs prefs = getDevicePrefs();
+ int equalizerDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"));
+
+ // Equalizer preset might be larger than 127 -> convert to unsigned int
+ ByteBuffer buf = ByteBuffer.wrap(payload);
+ byte adaptiveDirection = buf.get();
+ byte currentDirection = buf.get();
+ int equalizerPreset = Byte.toUnsignedInt(buf.get());
+ byte[] equalizer = new byte[18];
+
+ // Choose one of the three custom equalizer configurations
+ buf.position(buf.position() + equalizerDirection * equalizer.length);
+ buf.get(equalizer);
+
+ LOG.debug(
+ "Equalizer: adaptiveDirection={}, direction={}, preset={}",
+ adaptiveDirection, currentDirection, equalizerPreset
+ );
+
+ Map newPrefs = equalizerToPrefs(equalizer);
+ newPrefs.put(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, adaptiveDirection != (byte)0x00);
+ newPrefs.put(PREF_SOUNDCORE_EQUALIZER_PRESET, String.valueOf(equalizerPreset));
+
+ return new GBDeviceEvent[] {
+ new GBDeviceEventUpdatePreferences(newPrefs),
+ new GBDeviceEventUpdateDeviceState(GBDevice.State.INITIALIZED),
+ };
+ }
+
+ private GBDeviceEvent[] handlePacketAdaptiveDirection(byte[] payload) {
+ if (payload.length != 0)
+ return null;
+
+ Prefs prefs = getDevicePrefs();
+
+ // Ignore if adaptive direction is enabled
+ if (prefs.getBoolean(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, true))
+ return null;
+
+ // Set equalizer direction to "standing" and get current configuration
+ return new GBDeviceEvent[] {
+ new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"),
+ new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)),
+ };
+ }
+
+ private GBDeviceEvent[] handlePacketBatteryInfo(byte[] payload) {
+ if (payload.length != 1)
+ return null;
+
+ batteryInfo.level = payload[0] * 20;
+
+ return new GBDeviceEvent[] { batteryInfo };
+ }
+
+ private GBDeviceEvent[] handlePacketChargingInfo(byte[] payload) {
+ if (payload.length != 1)
+ return null;
+
+ batteryInfo.state = payload[0] == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING;
+
+ return new GBDeviceEvent[] { batteryInfo };
+ }
+
+ @Override
+ public byte[] encodeSendConfiguration(String config) {
+ Prefs prefs = getDevicePrefs();
+
+ switch (config) {
+ case PREF_SOUNDCORE_VOICE_PROMPTS:
+ return encodeSetBoolean(prefs, PREF_SOUNDCORE_VOICE_PROMPTS, CMD_SET_VOICE_PROMPTS);
+ case PREF_SOUNDCORE_BUTTON_BRIGHTNESS:
+ return encodeSetByte(prefs, PREF_SOUNDCORE_BUTTON_BRIGHTNESS, CMD_SET_BUTTON_BRIGHTNESS);
+ case PREF_SOUNDCORE_AUTO_POWER_OFF:
+ return encodeSetAutoPowerOff(prefs);
+ case PREF_SOUNDCORE_LDAC_MODE:
+ return encodeSetBoolean(prefs, PREF_SOUNDCORE_LDAC_MODE, CMD_SET_LDAC_MODE);
+ case PREF_SOUNDCORE_ADAPTIVE_DIRECTION:
+ return encodeSetBoolean(prefs, PREF_SOUNDCORE_ADAPTIVE_DIRECTION, CMD_SET_ADAPTIVE_DIRECTION);
+ case PREF_SOUNDCORE_EQUALIZER_PRESET:
+ return encodeSetByte(prefs, PREF_SOUNDCORE_EQUALIZER_PRESET, CMD_SET_EQUALIZER_PRESET);
+ case PREF_SOUNDCORE_EQUALIZER_DIRECTION:
+ return encodeRequest(CMD_GET_EQUALIZER);
+ case PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE:
+ case PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ:
+ case PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE:
+ return encodeSetEqualizerCustom(prefs);
+ }
+
+ return super.encodeSendConfiguration(config);
+ }
+
+ private byte[] encodeRequest(short cmd) {
+ return new SoundcorePacket(cmd).encode();
+ }
+
+ public byte[] encodeGetDeviceInfo() {
+ return encodeRequest(CMD_GET_DEVICE_INFO);
+ }
+
+ @Override
+ public byte[] encodePowerOff() {
+ return encodeRequest(CMD_POWER_OFF);
+ }
+
+ private byte[] encodeSetBoolean(Prefs prefs, String pref, short cmd) {
+ boolean enabled = prefs.getBoolean(pref, true);
+ byte[] payload = new byte[] { enabled ? (byte)0x01 : (byte)0x00 };
+
+ return new SoundcorePacket(cmd, payload).encode();
+ }
+
+ private byte[] encodeSetByte(Prefs prefs, String pref, short cmd) {
+ byte value = (byte)Integer.parseInt(prefs.getString(pref, "0"));
+ byte[] payload = new byte[] { value };
+
+ return new SoundcorePacket(cmd, payload).encode();
+ }
+
+ private byte[] encodeSetAutoPowerOff(Prefs prefs) {
+ byte duration = (byte)Integer.parseInt(prefs.getString(PREF_SOUNDCORE_AUTO_POWER_OFF, "2"));
+ byte[] payload;
+
+ if (duration > 0)
+ payload = new byte[] { (byte)0x01, (byte)(duration - 1) };
+ else
+ payload = new byte[] { (byte)0x00, (byte)0x00 };
+
+ return new SoundcorePacket(CMD_SET_AUTO_POWER_OFF, payload).encode();
+ }
+
+ private byte[] encodeSetEqualizerCustom(Prefs prefs) {
+ ByteBuffer buf = ByteBuffer.allocate(21);
+ int eqDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"));
+ byte[] equalizer = equalizerFromPrefs(prefs);
+
+ // Bit combination of the equalizer directions that should be changed
+ buf.put((byte)(1 << eqDirection));
+ buf.put((byte)0x01);
+ buf.put((byte)0xff);
+ buf.put(equalizer);
+
+ return new SoundcorePacket(CMD_SET_EQUALIZER_CUSTOM, buf.array()).encode();
+ }
+
+ private Map equalizerToPrefs(byte[] equalizer) {
+ Map prefs = new HashMap<>();
+
+ for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) {
+ // Equalizer values range from 60 to 180, with 120 as "neutral"
+ byte freq = equalizer[i * 2 + 1];
+ int value = Byte.toUnsignedInt(equalizer[i * 2]);
+
+ // Map equalizer value to preference range (0 to 120)
+ prefs.put(EQUALIZER_PREFS_FREQ[i], freq);
+ prefs.put(EQUALIZER_PREFS_VALUE[i], value - 60);
+ }
+
+ return prefs;
+ }
+
+ private byte[] equalizerFromPrefs(Prefs prefs) {
+ byte[] equalizer = new byte[EQUALIZER_PREFS_FREQ.length * 2];
+
+ for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) {
+ int freq = Integer.parseInt(prefs.getString(EQUALIZER_PREFS_FREQ[i], "0"));
+ int value = prefs.getInt(EQUALIZER_PREFS_VALUE[i], 60);
+
+ // Map equalizer values from 0 - 120 back to 60 - 180
+ equalizer[i * 2 + 1] = ((byte)freq);
+ equalizer[i * 2] = (byte)(value + 60);
+ }
+
+ return equalizer;
+ }
+}
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index e5e22cba1..113426168 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -3485,6 +3485,192 @@
- AMBIENT_SOUND_CONTROL
+
+ - @string/off
+ - @string/soundcore_button_brightness_low
+ - @string/soundcore_button_brightness_medium
+ - @string/soundcore_button_brightness_high
+
+
+
+ - 0
+ - 20
+ - 70
+ - 100
+
+
+
+ - @string/pref_button_action_disabled
+ - @string/minutes_5
+ - @string/minutes_10
+ - @string/minutes_20
+ - @string/minutes_60
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
+
+
+ - @string/soundcore_equalizer_preset_signature
+ - @string/soundcore_equalizer_preset_xtra_bass
+ - @string/soundcore_equalizer_preset_voice
+ - @string/soundcore_equalizer_preset_balanced
+ - @string/custom
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 254
+
+
+
+ - @string/soundcore_equalizer_direction_standing
+ - @string/soundcore_equalizer_direction_lying
+ - @string/soundcore_equalizer_direction_hanging
+
+
+
+ - 0
+ - 1
+ - 2
+
+
+
+ - 48 Hz
+ - 51 Hz
+ - 55 Hz
+ - 59 Hz
+ - 63 Hz
+ - 68 Hz
+ - 72 Hz
+ - 80 Hz
+ - 83 Hz
+ - 89 Hz
+
+
+
+ - 95 Hz
+ - 102 Hz
+ - 109 Hz
+ - 117 Hz
+ - 125 Hz
+ - 134 Hz
+ - 144 Hz
+ - 150 Hz
+ - 165 Hz
+ - 177 Hz
+
+
+
+ - 190 Hz
+ - 203 Hz
+ - 218 Hz
+ - 233 Hz
+ - 250 Hz
+ - 268 Hz
+ - 287 Hz
+ - 300 Hz
+ - 330 Hz
+ - 353 Hz
+
+
+
+ - 379 Hz
+ - 406 Hz
+ - 435 Hz
+ - 467 Hz
+ - 500 Hz
+ - 536 Hz
+ - 574 Hz
+ - 600 Hz
+ - 659 Hz
+ - 706 Hz
+
+
+
+ - 759 Hz
+ - 813 Hz
+ - 871 Hz
+ - 933 Hz
+ - 1.0 kHz
+ - 1.07 kHz
+ - 1.1 kHz
+ - 1.2 kHz
+ - 1.3 kHz
+ - 1.4 kHz
+
+
+
+ - 1.5 kHz
+ - 1.6 kHz
+ - 1.75 kHz
+ - 1.86 kHz
+ - 2.0 kHz
+ - 2.15 kHz
+ - 2.3 kHz
+ - 2.5 kHz
+ - 2.6 kHz
+ - 2.8 kHz
+
+
+
+ - 3.0 kHz
+ - 3.2 kHz
+ - 3.5 kHz
+ - 3.7 kHz
+ - 4.0 kHz
+ - 4.3 kHz
+ - 4.6 kHz
+ - 5.0 kHz
+ - 5.3 kHz
+ - 5.6 kHz
+
+
+
+ - 6.0 kHz
+ - 6.5 kHz
+ - 7.0 kHz
+ - 7.5 kHz
+ - 8.0 kHz
+ - 8.5 kHz
+ - 9.0 kHz
+ - 9.8 kHz
+ - 10.5 kHz
+ - 11.3 kHz
+
+
+
+ - 12.1 kHz
+ - 13.0 kHz
+ - 14.0 kHz
+ - 15.0 kHz
+ - 16.0 kHz
+ - 17.1 kHz
+ - 18.3 kHz
+ - 20.0 kHz
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+
+
- @string/moondrop_equalizer_preset_reference
- @string/moondrop_equalizer_preset_basshead
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 82dcd54aa..4c9e2fc98 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -925,7 +925,9 @@
1 minute
5 minutes
10 minutes
+ 20 minutes
30 minutes
+ 60 minutes
Live activity
Steps today, target: %1$s
Lack of steps: %1$d
@@ -1621,6 +1623,7 @@
Sony LinkBuds
Sony LinkBuds S
Soundcore Liberty 3 Pro
+ Soundcore Motion 300
Moondrop Space Travel
Binary sensor
Honor Band 3
@@ -2442,6 +2445,39 @@
Switch Active Noise Cancelling Mode
Long Press (1s)
Long Press (3s)
+ Voice Prompts
+ Button Brightness
+ Low
+ Medium
+ High
+ LDAC Mode
+ Enabling LDAC will decrease the battery life and might lead to connection instability
+ Adaptive Direction
+ Adjust equalizer preset automatically based on device direction
+ Preset
+ soundcore Signature
+ Xtra Bass
+ Voice
+ Balanced
+ Customize…
+ Configure parametric equalizer settings
+ Device Direction
+ Standing
+ Lying
+ Hanging
+ Reset to default
+ Set all equalizer bands back to default settings
+ Band 1
+ Band 2
+ Band 3
+ Band 4
+ Band 5
+ Band 6
+ Band 7
+ Band 8
+ Band 9
+ Frequency
+ Value
Protocol Version
Auto Brightness
Adjust screen brightness according to ambient light
diff --git a/app/src/main/res/xml/devicesettings_soundcore_motion300.xml b/app/src/main/res/xml/devicesettings_soundcore_motion300.xml
new file mode 100644
index 000000000..130bb9ce4
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_soundcore_motion300.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml b/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml
new file mode 100644
index 000000000..aebba11b4
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+