1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-01-26 17:47:34 +01:00

Soundcore Q30: Initial Support

This commit is contained in:
ahormann 2024-11-09 12:48:08 +01:00 committed by José Rebelo
parent 725c477ebd
commit 991461a8d8
12 changed files with 366 additions and 13 deletions

View File

@ -411,6 +411,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING = "pref_adaptive_noise_cancelling";
public static final String PREF_SOUNDCORE_WIND_NOISE_REDUCTION= "pref_soundcore_wind_noise_reduction";
public static final String PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE = "pref_soundcore_transparency_vocal_mode";
public static final String PREF_SOUNDCORE_ANC_MODE = "pref_soundcore_anc_mode";
public static final String PREF_SOUNDCORE_WEARING_DETECTION = "pref_soundcore_wearing_detection";
public static final String PREF_SOUNDCORE_WEARING_TONE = "pref_soundcore_wearing_tone";
public static final String PREF_SOUNDCORE_TOUCH_TONE = "pref_soundcore_touch_tone";

View File

@ -747,6 +747,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING);
addPreferenceHandlerFor(PREF_SOUNDCORE_ANC_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION);

View File

@ -0,0 +1,71 @@
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30;
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.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30.SoundcoreQ30DeviceSupport;
public class SoundcoreQ30Coordinator extends AbstractDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_soundcore_q30;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_headphones;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_headphones_disabled;
}
@Override
public String getManufacturer() {
return "Anker";
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Soundcore Q30");
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
BatteryConfig battery = new BatteryConfig(0, R.drawable.ic_battery, R.string.battery);
return new BatteryConfig[]{battery};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_q30);
return deviceSpecificSettings;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return SoundcoreQ30DeviceSupport.class;
}
}

View File

@ -268,6 +268,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoo
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc.SoundcoreLiberty4NCCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30.SoundcoreQ30Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
@ -536,6 +537,7 @@ public enum DeviceType {
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
SOUNDCORE_Q30(SoundcoreQ30Coordinator.class),
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
HONORBAND3(HonorBand3Coordinator.class),

View File

@ -76,15 +76,15 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol {
private void decodeAudioMode(byte[] payload) {
SharedPreferences prefs = getDevicePrefs().getPreferences();
SharedPreferences.Editor editor = prefs.edit();
String soundmode = "off";
String ambient_sound_mode = "off";
int anc_strength = 0;
if (payload[0] == 0x00) {
soundmode = "noise_cancelling";
ambient_sound_mode = "noise_cancelling";
} else if (payload[0] == 0x01) {
soundmode = "ambient_sound";
ambient_sound_mode = "ambient_sound";
} else if (payload[0] == 0x02) {
soundmode = "off";
ambient_sound_mode = "off";
}
if (payload[1] == 0x10) {
@ -99,7 +99,7 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol {
boolean adaptive_anc = (payload[3] == 0x01);
boolean windnoiseReduction = (payload[4] == 0x01);
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode);
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, ambient_sound_mode);
editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc);
@ -210,19 +210,19 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol {
private byte[] encodeAudioMode() {
Prefs prefs = getDevicePrefs();
byte anc_mode;
byte ambient_sound_mode;
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) {
case "noise_cancelling":
anc_mode = 0x00;
ambient_sound_mode = 0x00;
break;
case "ambient_sound":
anc_mode = 0x01;
ambient_sound_mode = 0x01;
break;
case "off":
anc_mode = 0x02;
ambient_sound_mode = 0x02;
break;
default:
LOG.error("Invalid Audio Mode selected");
LOG.error("Invalid Ambient Mode selected");
return null;
}
@ -246,7 +246,7 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol {
byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false));
byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false));
byte[] payload = new byte[]{anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
byte[] payload = new byte[]{ambient_sound_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
return new SoundcorePacket((short) 0x8106, payload).encode();
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreQ30DeviceSupport extends AbstractSerialDeviceSupport {
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new SoundcoreQ30Protocol(getDevice());
}
@Override
protected GBDeviceIoThread createDeviceIOThread() {
return new SoundcoreQ30IOThread(getDevice(), getContext(), (SoundcoreQ30Protocol) getDeviceProtocol(),this, getBluetoothAdapter());
}
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override
public boolean useAutoConnect() {
return false;
}
}

View File

@ -0,0 +1,52 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
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 nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty.SoundcoreLibertyProtocol;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreQ30IOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreQ30IOThread.class);
private final SoundcoreQ30Protocol mSoundcoreProtocol;
public SoundcoreQ30IOThread(GBDevice gbDevice, Context context, SoundcoreQ30Protocol deviceProtocol, AbstractSerialDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
mSoundcoreProtocol = deviceProtocol;
}
@Override
protected byte[] parseIncoming(InputStream inStream) throws IOException {
byte[] buffer = new byte[1048576]; //HUGE read
int bytes = inStream.read(buffer);
LOG.debug("read {} bytes. {}", bytes, hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
@Override
protected void initialize() {
write(mSoundcoreProtocol.encodeDeviceInfoRequest());
super.initialize();
}
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
}
}

View File

@ -0,0 +1,157 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.AbstractSoundcoreProtocol;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SoundcoreQ30Protocol extends AbstractSoundcoreProtocol {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreQ30Protocol.class);
protected SoundcoreQ30Protocol(GBDevice device) {
super(device);
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
ByteBuffer buf = ByteBuffer.wrap(responseData);
SoundcorePacket packet = SoundcorePacket.decode(buf);
if (packet == null)
return null;
List<GBDeviceEvent> devEvts = new ArrayList<>();
short cmd = packet.getCommand();
byte[] payload = packet.getPayload();
if (cmd == (short) 0x0101) {
// a lot of other data is in here, anything interesting?
String firmware1 = readString(payload, 39, 5);
String firmware2 = "";
String serialNumber = readString(payload, 44, 16);
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
} else if (cmd == (short) 0x0106) { // ANC Mode Updated by Button
decodeAudioMode(payload);
} else if (cmd == (short) 0x0301) { // Battery Update
int battery = payload[0] * 20; // untested
devEvts.add(buildBatteryInfo(0, battery));
} else if (cmd == (short) 0x8106) {
// Acknowledgement for changed Ambient Mode
// empty payload
} else {
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
}
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
}
@Override
public byte[] encodeSendConfiguration(String config) {
switch (config) {
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE:
return encodeAudioMode();
default:
LOG.debug("Unsupported CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
/**
* Encodes the following settings to a payload to set the audio-mode on the headphones:
* PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active
* PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise
* PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode
* PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent
* PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise
* @return The payload
*/
private byte[] encodeAudioMode() {
Prefs prefs = getDevicePrefs();
byte ambient_sound_mode;
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) {
case "noise_cancelling":
ambient_sound_mode = 0x00;
break;
case "ambient_sound":
ambient_sound_mode = 0x01;
break;
case "off":
ambient_sound_mode = 0x02;
break;
default:
LOG.error("Invalid Ambient Mode selected");
return null;
}
byte anc_mode;
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE, "transport")) {
case "transport":
anc_mode = 0x00;
break;
case "outdoor":
anc_mode = 0x01;
break;
case "indoor":
anc_mode = 0x02;
break;
default:
LOG.error("Invalid ANC Mode selected");
return null;
}
byte[] payload = new byte[]{ambient_sound_mode, anc_mode, 0x01};
return new SoundcorePacket((short) 0x8106, payload).encode();
}
/**
* Gets triggered when the button on the device is pressed or transparency toggled with the right palm.
*/
private void decodeAudioMode(byte[] payload) {
SharedPreferences prefs = getDevicePrefs().getPreferences();
SharedPreferences.Editor editor = prefs.edit();
String ambient_sound_mode = "off";
String anc_mode = "transport";
if (payload[0] == 0x00) {
ambient_sound_mode = "noise_cancelling";
} else if (payload[0] == 0x01) {
ambient_sound_mode = "ambient_sound";
} else if (payload[0] == 0x02) {
ambient_sound_mode = "off";
}
if (payload[1] == 0x00) {
anc_mode = "transport";
} else if (payload[1] == 0x01) {
anc_mode = "outdoor";
} else if (payload[1] == 0x02) {
anc_mode = "indoor";
}
// payload has two more bytes
// payload[2] always 1 ?
// payload[3] checksum ?
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, ambient_sound_mode);
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE, anc_mode);
editor.apply();
}
}

View File

@ -3762,6 +3762,18 @@
<item>game_mode</item>
</string-array>
<string-array name="soundcore_anc_mode_names">
<item>@string/prefs_active_noise_cancelling_transport</item>
<item>@string/prefs_active_noise_cancelling_indoor</item>
<item>@string/prefs_active_noise_cancelling_outdoor</item>
</string-array>
<string-array name="soundcore_anc_mode_values">
<item>transport</item>
<item>indoor</item>
<item>outdoor</item>
</string-array>
<string-array name="soundcore_button_function_names">
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_volumeup</item>

View File

@ -1860,6 +1860,7 @@
<string name="devicetype_soundcore_liberty3_pro">Soundcore Liberty 3 Pro</string>
<string name="devicetype_soundcore_liberty4_nc">Soundcore Liberty 4 NC</string>
<string name="devicetype_soundcore_motion300">Soundcore Motion 300</string>
<string name="devicetype_soundcore_q30">Soundcore Q30</string>
<string name="devicetype_moondrop_space_travel">Moondrop Space Travel</string>
<string name="devicetype_binary_sensor">Binary sensor</string>
<string name="devicetype_honor_band3">Honor Band 3</string>
@ -2580,6 +2581,9 @@
<string name="prefs_active_noise_cancelling_level_high">High</string>
<string name="prefs_active_noise_cancelling_level_low">Low</string>
<string name="prefs_active_noise_cancelling_summary">Block noises of the surroundings</string>
<string name="prefs_active_noise_cancelling_transport">Transport</string>
<string name="prefs_active_noise_cancelling_indoor">Indoor</string>
<string name="prefs_active_noise_cancelling_outdoor">Outdoor</string>
<string name="prefs_pressure_relief">Pressure relief with ambient sound</string>
<string name="pressure_relief_summary">Prevent feeling of pressure in ears when not using Active Noise Cancelling</string>
<string name="prefs_left">Left</string>

View File

@ -48,8 +48,6 @@
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_header_soundcore_other"
android:title="@string/pref_header_other">

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_header_soundcore_ambient_sound_control"
android:title="@string/pref_header_sony_ambient_sound_control">
<ListPreference
android:defaultValue="noise_cancelling"
android:entries="@array/sony_ambient_sound_control_names"
android:entryValues="@array/sony_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_soundcore_ambient_sound_control"
android:summary="%s"
android:title="@string/sony_ambient_sound" />
<ListPreference
android:defaultValue="transport"
android:entries="@array/soundcore_anc_mode_names"
android:entryValues="@array/soundcore_anc_mode_values"
android:icon="@drawable/ic_hearing"
android:key="pref_soundcore_anc_mode"
android:summary="%s"
android:title="@string/prefs_active_noise_cancelling_level" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>