1MORE SonoFlow: Initial support (#4637)

Co-authored-by: pacjo <kamil.kroliczek2004@gmail.com>
Co-committed-by: pacjo <kamil.kroliczek2004@gmail.com>
This commit is contained in:
pacjo
2025-03-15 21:00:48 +00:00
committed by José Rebelo
parent d5af0da3f9
commit dda96a9dd3
13 changed files with 527 additions and 0 deletions
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_devices_other"
android:key="dual_device_support"
android:layout="@layout/preference_checkbox"
android:title="@string/dual_device_mode_title"
android:summary="@string/dual_device_mode_summary" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_music_note"
android:key="pref_soundcore_ldac_mode"
android:layout="@layout/preference_checkbox"
android:title="@string/soundcore_ldac_mode_title"
android:summary="@string/soundcore_ldac_mode_summary" />
</androidx.preference.PreferenceScreen>
@@ -107,6 +107,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_DEVICE_INTENTS = "device_intents";
public static final String PREF_ACTIVE_NOISE_CANCELLING_TOGGLE = "active_noise_cancelling_toggle";
public static final String PREF_NOISE_CONTROL_SELECTOR = "noise_control_selector";
public static final String PREF_WEAR_SENSOR_TOGGLE = "wear_sensor_toggle";
public static final String PREF_BANDW_PSERIES_VPT_ENABLED = "bandw_pseries_vpt_enabled";
public static final String PREF_BANDW_PSERIES_VPT_LEVEL = "bandw_pseries_vpt_level";
@@ -520,6 +521,8 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_QC35_NOISE_CANCELLING_LEVEL = "qc35_noise_cancelling_level";
public static final String PREF_DUAL_DEVICE_SUPPORT = "dual_device_support";
public static final String PREFS_ACTIVITY_IN_DEVICE_CARD = "prefs_activity_in_device_card";
public static final String PREFS_ACTIVITY_IN_DEVICE_CARD_STEPS = "prefs_activity_in_device_card_steps";
public static final String PREFS_ACTIVITY_IN_DEVICE_CARD_SLEEP = "prefs_activity_in_device_card_sleep";
@@ -614,6 +614,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SLEEP_MODE_SMART_ENABLE);
addPreferenceHandlerFor(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE);
addPreferenceHandlerFor(PREF_NOISE_CONTROL_SELECTOR);
addPreferenceHandlerFor(PREF_WEAR_SENSOR_TOGGLE);
addPreferenceHandlerFor(PREF_BANDW_PSERIES_GUI_VPT_LEVEL);
@@ -816,6 +817,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);
addPreferenceHandlerFor(PREF_DUAL_DEVICE_SUPPORT);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_NOTIFICATION);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_SECONDARY);
@@ -0,0 +1,96 @@
package nodomain.freeyourgadget.gadgetbridge.devices.onemoresonoflow;
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.DeviceSpecificSettingsScreen;
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.onemore_sonoflow.OneMoreSonoFlowSupport;
public class OneMoreSonoFlowCoordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("1MORE SonoFlow");
}
@Override
public String getManufacturer() {
return "1MORE";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return OneMoreSonoFlowSupport.class;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_onemore_sonoflow;
}
@Override
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
return new BatteryConfig[] {
new BatteryConfig(
0,
GBDevice.BATTERY_ICON_DEFAULT,
GBDevice.BATTERY_LABEL_DEFAULT,
20,
100
)
};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
settings.addRootScreen(DeviceSpecificSettingsScreen.SOUND);
settings.addSubScreen(
DeviceSpecificSettingsScreen.SOUND,
R.xml.devicesettings_onemore_noise_control_selector
);
settings.addSubScreen(
DeviceSpecificSettingsScreen.SOUND,
R.xml.devicesettings_ldac_toggle
);
settings.addRootScreen(
DeviceSpecificSettingsScreen.CONNECTION,
R.xml.devicesettings_dual_device_toggle
);
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
settings.addSubScreen(
DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS,
R.xml.devicesettings_headphones
);
return settings;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_headphones;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_headphones_disabled;
}
}
@@ -29,6 +29,7 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.onemoresonoflow.OneMoreSonoFlowCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.asteroidos.AsteroidOSDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries.BandWPSeriesDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
@@ -563,6 +564,7 @@ public enum DeviceType {
SOUNDCORE_Q30(SoundcoreQ30Coordinator.class),
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
ONEMORE_SONOFLOW(OneMoreSonoFlowCoordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
HONORBAND4(HonorBand4Coordinator.class),
HONORBAND5(HonorBand5Coordinator.class),
@@ -0,0 +1,129 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.onemore_sonoflow;
public class OneMorePacket {
// sent from phone
public static final byte[] REQUEST_PREAMBLE = { 0x11, 0x01, 0x00 };
// sent from headphones
public static final byte[] RESPONSE_PREAMBLE = { 0x01, 0x01, 0x00 };
// battery and firmware version (maybe more?)
public static final byte GET_DEVICE_INFO_COMMAND = 0x4e;
public static final byte GET_NOISE_CONTROL_COMMAND = 0x5f;
public static final byte SET_NOISE_CONTROL_COMMAND = 0x5e;
public static final byte GET_LDAC_COMMAND = 0x6c;
public static final byte SET_LDAC_COMMAND = 0x6b;
public static final byte GET_DUAL_DEVICE_COMMAND = 0x77;
public static final byte SET_DUAL_DEVICE_COMMAND = 0x76;
// TODO:
// - figure out flags
// - figure out checksums
public static byte[] createGetDeviceInfoPacket() {
byte[] flags = { 0x00, 0x00, 0x00 };
byte[] checksum = { 0x1c, 0x42 };
return concat(REQUEST_PREAMBLE, GET_DEVICE_INFO_COMMAND, flags, checksum);
}
public static byte[] createGetNoiseControlModePacket() {
byte[] flags = { 0x00, 0x00, 0x00 };
byte[] checksum = { 0x0c, 0x43 };
return concat(REQUEST_PREAMBLE, GET_NOISE_CONTROL_COMMAND, flags, checksum);
}
public static byte[] createSetNoiseControlModePacket(String mode) {
byte[] flags = { 0x00, 0x01, 0x00 };
byte[] checksum = { 0x13, 0x5c };
byte modeByte;
switch (mode) {
case "0":
// Off
modeByte = 0x00;
break;
case "1":
// ANC
modeByte = 0x01;
break;
case "2":
// Pass-through
modeByte = 0x03;
break;
default:
throw new IllegalStateException("mode not one of the allowed values: [\"0\", \"1\", \"2\"]");
}
return concat(REQUEST_PREAMBLE, SET_NOISE_CONTROL_COMMAND, flags, checksum, modeByte);
}
public static byte[] createGetLdacModePacket() {
byte[] flags = { 0x00, 0x00, 0x00 };
byte[] checksum = { 0x0d, 0x71 };
return concat(REQUEST_PREAMBLE, GET_LDAC_COMMAND, flags, checksum);
}
public static byte[] createSetLdacModePacket(boolean enabled) {
byte[] flags = { 0x00, 0x01, 0x00 };
byte[] checksum = { 0x2d, 0x57 };
byte modeByte = (byte) (enabled ? 0x02 : 0x00);
return concat(REQUEST_PREAMBLE, SET_LDAC_COMMAND, flags, checksum, modeByte);
}
public static byte[] createGetDualDeviceModePacket() {
byte[] flags = { 0x00, 0x00, 0x00 };
byte[] checksum = { 0x0c, 0x6b };
return concat(REQUEST_PREAMBLE, GET_DUAL_DEVICE_COMMAND, flags, checksum);
}
public static byte[] createSetDualDeviceModePacket(boolean enabled) {
byte[] flags = { 0x00, 0x01, 0x00 };
byte[] checksum = { 0x0d, 0x6a };
byte modeByte = (byte) (enabled ? 0x01 : 0x00);
return concat(REQUEST_PREAMBLE, SET_DUAL_DEVICE_COMMAND, flags, checksum, modeByte);
}
private static byte[] concat(Object... args) {
if (args == null || args.length == 0) {
return new byte[0];
}
int totalLength = 0;
for (Object arg : args) {
if (arg instanceof byte[]) {
totalLength += ((byte[]) arg).length;
} else if (arg instanceof Byte) {
totalLength++;
} else {
throw new IllegalArgumentException("Invalid argument type: " + arg.getClass().getName() + ". Expected byte[] or Byte.");
}
}
byte[] result = new byte[totalLength];
int offset = 0;
for (Object arg : args) {
if (arg instanceof byte[]) {
byte[] byteArray = (byte[]) arg;
System.arraycopy(byteArray, 0, result, offset, byteArray.length);
offset += byteArray.length;
} else if (arg instanceof Byte) {
result[offset++] = (Byte) arg;
}
}
return result;
}
}
@@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.onemore_sonoflow;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
public class OneMoreSonoFlowIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(OneMoreSonoFlowIOThread.class);
public OneMoreSonoFlowIOThread(
GBDevice gbDevice,
Context context,
OneMoreSonoFlowProtocol deviceProtocol,
AbstractSerialDeviceSupport deviceSupport,
BluetoothAdapter btAdapter
) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
}
@Override
protected void initialize() {
super.initialize();
// get some device information
// TODO: we might not receive some responses, it might be worth requesting them again if that's a significant issue
// https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/4637#issuecomment-3035556
write(OneMorePacket.createGetDeviceInfoPacket());
write(OneMorePacket.createGetNoiseControlModePacket());
write(OneMorePacket.createGetLdacModePacket());
write(OneMorePacket.createGetDualDeviceModePacket());
setUpdateState(GBDevice.State.INITIALIZED);
}
@Override
protected byte[] parseIncoming(InputStream stream) throws IOException {
byte[] buffer = new byte[1048576]; // big value
int bytes = stream.read(buffer);
LOG.debug("read {} bytes. {}", bytes, hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
}
@@ -0,0 +1,163 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.onemore_sonoflow;
import static nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.startsWith;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class OneMoreSonoFlowProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(OneMoreSonoFlowProtocol.class);
protected OneMoreSonoFlowProtocol(GBDevice device) {
super(device);
}
@Override
public byte[] encodeSendConfiguration(String config) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
switch (config) {
case DeviceSettingsPreferenceConst.PREF_NOISE_CONTROL_SELECTOR:
return OneMorePacket.createSetNoiseControlModePacket(prefs.getString(config, "0"));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_LDAC_MODE:
return OneMorePacket.createSetLdacModePacket(prefs.getBoolean(config, false));
case DeviceSettingsPreferenceConst.PREF_DUAL_DEVICE_SUPPORT:
return OneMorePacket.createSetDualDeviceModePacket(prefs.getBoolean(config, false));
}
return super.encodeSendConfiguration(config);
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
LOG.debug("decodeResponse: got: {}", Arrays.toString(responseData));
List<GBDeviceEvent> events = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.wrap(responseData);
buffer.order(ByteOrder.BIG_ENDIAN);
while (buffer.position() < buffer.limit()) {
if (!startsWith(buffer.array(), OneMorePacket.RESPONSE_PREAMBLE)) {
LOG.warn("Unknown preamble, skipping byte");
// skip a byte and try again
buffer.position(buffer.position() + 1);
continue;
}
LOG.debug("Preamble found, processing packet");
// skip too short packets (shortest recorded packet has 10 bytes)
if (buffer.remaining() < 10) {
LOG.warn("Incomplete packet (less than 10 bytes remaining), ignoring");
break;
}
byte command = buffer.get(buffer.position() + 3);
if (buffer.remaining() >= 6 && command == OneMorePacket.GET_NOISE_CONTROL_COMMAND) {
LOG.debug("Handling noise control packet");
events.add(decodeNoiseControlMode(buffer.get(9)));
buffer.position(buffer.position() + 10);
} else if (buffer.remaining() >= 6 && command == OneMorePacket.GET_LDAC_COMMAND) {
LOG.debug("Handling LDAC packet");
events.add(decodeLdacMode(buffer.get(9)));
buffer.position(buffer.position() + 10);
} else if (buffer.remaining() >= 6 && command == OneMorePacket.GET_DUAL_DEVICE_COMMAND) {
LOG.debug("Handling dual device packet");
events.add(decodeDualDeviceMode(buffer.get(9)));
buffer.position(buffer.position() + 10);
} else if (buffer.remaining() >= 10 && command == OneMorePacket.GET_DEVICE_INFO_COMMAND) {
LOG.debug("Handling battery info packet");
events.add(decodeBatteryInfo(buffer.get(13)));
events.add(decodeFirmwareInformation(buffer.get(10), buffer.get(11), buffer.get(12)));
buffer.position(buffer.position() + 19);
} else {
LOG.debug("Unknown packet command: 0x{} with buffer: {}, starting at: {}, ignoring packet", Integer.toHexString(command), Arrays.toString(buffer.array()), buffer.position());
// skip a byte and try again
buffer.position(buffer.position() + 1);
}
}
return events.toArray(new GBDeviceEvent[0]);
}
private GBDeviceEventUpdatePreferences decodeNoiseControlMode(byte value) {
GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences();
String mode = "0";
switch (value) {
case 0x00:
mode = "0";
break;
case 0x01:
mode = "1";
break;
case 0x03:
mode = "2";
break;
}
event.withPreference(DeviceSettingsPreferenceConst.PREF_NOISE_CONTROL_SELECTOR, mode);
return event;
}
private GBDeviceEventUpdatePreferences decodeLdacMode(byte value) {
GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences();
boolean enabled = value == 0x02;
event.withPreference(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_LDAC_MODE, enabled);
return event;
}
private GBDeviceEventUpdatePreferences decodeDualDeviceMode(byte value) {
GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences();
boolean enabled = value == 0x01;
event.withPreference(DeviceSettingsPreferenceConst.PREF_DUAL_DEVICE_SUPPORT, enabled);
return event;
}
@SuppressLint("DefaultLocale")
private GBDeviceEventVersionInfo decodeFirmwareInformation(byte major, byte minor, byte patch) {
GBDeviceEventVersionInfo event = new GBDeviceEventVersionInfo();
event.fwVersion = String.format("%d.%d.%d", major, minor, patch);;
return event;
}
private GBDeviceEventBatteryInfo decodeBatteryInfo(byte value) {
GBDeviceEventBatteryInfo event = new GBDeviceEventBatteryInfo();
event.level = value;
return event;
}
}
@@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.onemore_sonoflow;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class OneMoreSonoFlowSupport extends AbstractHeadphoneDeviceSupport {
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new OneMoreSonoFlowProtocol(getDevice());
}
@Override
protected GBDeviceIoThread createDeviceIOThread() {
return new OneMoreSonoFlowIOThread(
getDevice(),
getContext(),
(OneMoreSonoFlowProtocol) getDeviceProtocol(),
OneMoreSonoFlowSupport.this,
getBluetoothAdapter()
);
}
@Override
public boolean useAutoConnect() {
return false;
}
}
+12
View File
@@ -3397,6 +3397,18 @@
<item>3</item>
</string-array>
<string-array name="onemore_noise_control_modes">
<item>@string/off</item>
<item>@string/prefs_active_noise_cancelling</item>
<item>@string/prefs_active_noise_cancelling_transparency</item>
</string-array>
<string-array name="onemore_noise_control_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="redmi_buds_3_pro_equalizer_presets_names">
<item>@string/redmi_buds_5_pro_equalizer_preset_standard</item>
<item>@string/redmi_buds_5_pro_equalizer_preset_treble</item>
+4
View File
@@ -2979,6 +2979,7 @@
<string name="qhybrid_summary_on_device_confirmation">On-device pairing confirmations can get annoying. Disabling them might lose you functionality.</string>
<string name="devicetype_vesc">VESC</string>
<string name="devicetype_bose_qc35">Bose QC35</string>
<string name="devicetype_onemore_sonoflow">1MORE SonoFlow</string>
<string name="devicetype_sony_wena3">Sony Wena 3</string>
<string name="withings_steel_hr">Withings Steel HR</string>
<string name="pref_button_action_disabled">Disabled</string>
@@ -3242,6 +3243,9 @@
<string name="purple">Purple</string>
<string name="white">White</string>
<string name="dual_device_mode_title">Dual device mode</string>
<string name="dual_device_mode_summary">Allow connecting headphones to two devices simultaneously</string>
<string name="prefs_wena3_title_screen">Display Settings</string>
<string name="prefs_wena3_title_activity">Activity Settings</string>
<string name="prefs_wena3_item_background_sync">Background Activity Data Sync</string>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="0"
android:entries="@array/onemore_noise_control_modes"
android:entryValues="@array/onemore_noise_control_values"
android:icon="@drawable/ic_surround"
android:key="noise_control_selector"
android:summary="%s"
android:title="@string/prefs_noise_control" />
</androidx.preference.PreferenceScreen>