mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-26 03:46:49 +01:00
Add initial support for Nothing Ear(1) TWS (#2403)
Nothing Ear (1) are wireless earbuds that support active noise suppression, transparency mode and several gestures. This initial commit adds support for: - reading battery level - setting audio mode - setting in-ear auto detection Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2403 Co-authored-by: daniele <daniele@noreply.codeberg.org> Co-committed-by: daniele <daniele@noreply.codeberg.org>
This commit is contained in:
parent
a6073fa938
commit
a7f42f0c4f
@ -95,6 +95,9 @@ public class DeviceSettingsPreferenceConst {
|
|||||||
public static final String PREF_BT_CONNECTED_ADVERTISEMENT = "bt_connected_advertisement";
|
public static final String PREF_BT_CONNECTED_ADVERTISEMENT = "bt_connected_advertisement";
|
||||||
public static final String PREF_TRANSLITERATION_ENABLED = "pref_transliteration_enabled";
|
public static final String PREF_TRANSLITERATION_ENABLED = "pref_transliteration_enabled";
|
||||||
|
|
||||||
|
public static final String PREF_NOTHING_EAR1_INEAR = "pref_nothing_inear_detection";
|
||||||
|
public static final String PREF_NOTHING_EAR1_AUDIOMODE = "pref_nothing_audiomode";
|
||||||
|
|
||||||
public static final String PREF_SOUNDS = "sounds";
|
public static final String PREF_SOUNDS = "sounds";
|
||||||
public static final String PREF_AUTH_KEY = "authkey";
|
public static final String PREF_AUTH_KEY = "authkey";
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
|
|||||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE;
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION;
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE;
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE;
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE;
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
|
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
|
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
|
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
|
||||||
@ -440,6 +442,9 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
|
|||||||
addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION);
|
addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION);
|
||||||
addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL);
|
addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL);
|
||||||
|
|
||||||
|
addPreferenceHandlerFor(PREF_NOTHING_EAR1_INEAR);
|
||||||
|
addPreferenceHandlerFor(PREF_NOTHING_EAR1_AUDIOMODE);
|
||||||
|
|
||||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||||
boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
|
boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
|
||||||
|
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||||
|
|
||||||
|
public class Ear1Coordinator extends AbstractDeviceCoordinator {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||||
|
if(candidate.getName().equals("Nothing ear (1)"))
|
||||||
|
return DeviceType.NOTHING_EAR1;
|
||||||
|
return DeviceType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceType getDeviceType() {
|
||||||
|
return DeviceType.NOTHING_EAR1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getPairingActivity() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsActivityDataFetching() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsActivityTracking() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsScreenshots() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAlarmSlotCount() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsSmartWakeup(GBDevice device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturer() {
|
||||||
|
return "Nothing";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsAppsManagement() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getAppsManagementActivity() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCalendarEvents() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRealtimeData() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsWeather() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsFindDevice() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||||
|
return new int[] {
|
||||||
|
R.xml.devicesettings_nothing_ear1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -100,6 +100,7 @@ public enum DeviceType {
|
|||||||
WASPOS(330, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_waspos),
|
WASPOS(330, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_waspos),
|
||||||
UM25(350, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_um25),
|
UM25(350, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_um25),
|
||||||
DOMYOS_T540(400, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_domyos_t540),
|
DOMYOS_T540(400, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_domyos_t540),
|
||||||
|
NOTHING_EAR1(410, R.drawable.ic_device_nothingear, R.drawable.ic_device_nothingear_disabled, R.string.devicetype_nothingear1),
|
||||||
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
|
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
|
||||||
|
|
||||||
private final int key;
|
private final int key;
|
||||||
|
@ -80,6 +80,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.nothing.Ear1Support;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport;
|
||||||
@ -358,6 +359,9 @@ public class DeviceSupportFactory {
|
|||||||
case FITPRO:
|
case FITPRO:
|
||||||
deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||||
break;
|
break;
|
||||||
|
case NOTHING_EAR1:
|
||||||
|
deviceSupport = new ServiceDeviceSupport(new Ear1Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (deviceSupport != null) {
|
if (deviceSupport != null) {
|
||||||
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
|
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||||
|
|
||||||
|
public class Ear1Support extends AbstractSerialDeviceSupport {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Ear1Support.class);
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendConfiguration(String config) {
|
||||||
|
super.onSendConfiguration(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInstallApp(Uri uri) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHeartRateTest() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetConstantVibration(int integer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReadConfiguration(String config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTestNewFunction() {
|
||||||
|
//getDeviceIOThread().write(((NothingProtocol) getDeviceProtocol()).encodeBatteryStatusReq());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean connect() {
|
||||||
|
getDeviceIOThread().start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized NothingIOThread getDeviceIOThread() {
|
||||||
|
return (NothingIOThread) super.getDeviceIOThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean useAutoConnect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GBDeviceProtocol createDeviceProtocol() {
|
||||||
|
return new NothingProtocol(getDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GBDeviceIoThread createDeviceIOThread() {
|
||||||
|
return new NothingIOThread(getDevice(), getContext(), (NothingProtocol) getDeviceProtocol(), Ear1Support.this, getBluetoothAdapter());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
|
||||||
|
|
||||||
|
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 static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||||
|
|
||||||
|
public class NothingIOThread extends BtClassicIoThread {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(NothingIOThread.class);
|
||||||
|
|
||||||
|
private final NothingProtocol mNothingProtocol;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
|
||||||
|
return mNothingProtocol.UUID_DEVICE_CTRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NothingIOThread(GBDevice device, Context context, NothingProtocol deviceProtocol, Ear1Support ear1Support, BluetoothAdapter bluetoothAdapter) {
|
||||||
|
super(device, context, deviceProtocol, ear1Support, bluetoothAdapter);
|
||||||
|
mNothingProtocol = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,235 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||||
|
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||||
|
|
||||||
|
public class NothingProtocol extends GBDeviceProtocol {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(NothingProtocol.class);
|
||||||
|
|
||||||
|
final UUID UUID_DEVICE_CTRL = UUID.fromString("aeac4a03-dff5-498f-843a-34487cf133eb");
|
||||||
|
|
||||||
|
|
||||||
|
public static final byte CONTROL_DEVICE_TYPE_TWS_HEADSET = 1;
|
||||||
|
|
||||||
|
private static final int CONTROL_CRC = 0x20;
|
||||||
|
|
||||||
|
private static final byte MASK_RSP_CODE = 0x1f;
|
||||||
|
private static final short MASK_DEVICE_TYPE = 0x0F00;
|
||||||
|
|
||||||
|
|
||||||
|
private static final short MASK_REQUEST_CMD = (short) 0x8000;
|
||||||
|
|
||||||
|
private static final byte MASK_BATTERY = 0x7f;
|
||||||
|
private static final byte MASK_BATTERY_CHARGING = (byte) 0x80;
|
||||||
|
|
||||||
|
|
||||||
|
//incoming
|
||||||
|
private static final short battery_status = (short) 0xe001;
|
||||||
|
private static final short battery_status2 = (short) 0xc007;
|
||||||
|
|
||||||
|
private static final short unk_maybe_ack = (short) 0xf002;
|
||||||
|
private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
|
||||||
|
|
||||||
|
//outgoing
|
||||||
|
private static final short in_ear_detection = (short) 0xf004;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||||
|
ByteBuffer incoming = ByteBuffer.wrap(responseData);
|
||||||
|
incoming.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
byte sof = incoming.get();
|
||||||
|
if (sof != 0x55) {
|
||||||
|
LOG.error("Error in message, wrong start of frame: " + hexdump(responseData));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
short control = incoming.getShort();
|
||||||
|
if (!isSupportedDevice(control)) {
|
||||||
|
LOG.error("Unsupported device specified in message: " + hexdump(responseData));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isOk(control)) {
|
||||||
|
LOG.error("Message is not ok: " + hexdump(responseData));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
short command = incoming.getShort();
|
||||||
|
short length = incoming.getShort();
|
||||||
|
incoming.get();
|
||||||
|
|
||||||
|
byte[] payload = Arrays.copyOfRange(responseData, incoming.position(), incoming.position() + length);
|
||||||
|
|
||||||
|
|
||||||
|
switch (getRequestCommand(command)) {
|
||||||
|
case battery_status:
|
||||||
|
case battery_status2:
|
||||||
|
return handleBatteryInfo(payload);
|
||||||
|
|
||||||
|
case unk_maybe_ack:
|
||||||
|
LOG.debug("received ack");
|
||||||
|
break;
|
||||||
|
case unk_close_case:
|
||||||
|
LOG.debug("case closed");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG.debug("Incoming message - control:" + control + " requestCommand: " + (getRequestCommand(command) & 0xffff) + "length: " + length + " dump: " + hexdump(responseData));
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isCrcNeeded(short control) {
|
||||||
|
return (control & CONTROL_CRC) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] encodeMessage(short control, short command, byte[] payload) {
|
||||||
|
|
||||||
|
ByteBuffer msgBuf = ByteBuffer.allocate(8 + payload.length);
|
||||||
|
msgBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
msgBuf.put((byte) 0x55); //sof
|
||||||
|
msgBuf.putShort(control);
|
||||||
|
msgBuf.putShort(command);
|
||||||
|
msgBuf.putShort((short) payload.length);
|
||||||
|
msgBuf.put((byte) 0x00); //fsn TODO: is this always 0?
|
||||||
|
msgBuf.put(payload);
|
||||||
|
|
||||||
|
if (isCrcNeeded(control)) {
|
||||||
|
msgBuf.position(0);
|
||||||
|
ByteBuffer crcBuf = ByteBuffer.allocate(msgBuf.capacity() + 2);
|
||||||
|
crcBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
crcBuf.put(msgBuf);
|
||||||
|
crcBuf.putShort((short) getCRC16ansi(msgBuf.array()));
|
||||||
|
return crcBuf.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgBuf.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encodeBatteryStatusReq() {
|
||||||
|
return encodeMessage((short) 0x120, (short) 0xc007, new byte[]{});
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encodeAudioMode(String desired) {
|
||||||
|
byte[] payload = new byte[]{0x01, 0x05, 0x00};
|
||||||
|
|
||||||
|
switch (desired) {
|
||||||
|
case "anc":
|
||||||
|
payload[1] = 0x01;
|
||||||
|
break;
|
||||||
|
case "transparency":
|
||||||
|
payload[1] = 0x07;
|
||||||
|
break;
|
||||||
|
case "off":
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return encodeMessage((short) 0x120, (short) 0xf00f, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encodeFindDevice(boolean start) {
|
||||||
|
byte payload = (byte) (start ? 0x01 : 0x00);
|
||||||
|
return encodeMessage((short) 0x120, (short) 0xf002, new byte[]{payload});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encodeSendConfiguration(String config) {
|
||||||
|
|
||||||
|
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||||
|
|
||||||
|
switch (config) {
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR:
|
||||||
|
byte enabled = (byte) (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR, true) ? 0x01 : 0x00);
|
||||||
|
return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
|
||||||
|
// response: 55 20 01 04 70 00 00 00
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE:
|
||||||
|
return encodeAudioMode(prefs.getString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off"));
|
||||||
|
// response: 55 20 01 0F 70 00 00 00
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG.debug("CONFIG: " + config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.encodeSendConfiguration(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encodeSetTime() {
|
||||||
|
// This are earphones, there is no time to set here. However this method gets called soon
|
||||||
|
// after connecting, hence we use it to perform some initializations.
|
||||||
|
return encodeBatteryStatusReq();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GBDeviceEvent[] handleBatteryInfo(byte[] payload) {
|
||||||
|
//LOG.debug("Battery payload: " + hexdump(payload));
|
||||||
|
|
||||||
|
/* payload:
|
||||||
|
1st byte is number of batteries, then $number pairs follow:
|
||||||
|
{idx, value}
|
||||||
|
|
||||||
|
idx is 0x02 for left ear, 0x03 for right ear, 0x04 for case
|
||||||
|
value goes from 0-64 (equivalent of 0-100 in hexadecimal)
|
||||||
|
|
||||||
|
|
||||||
|
Since Gadgetbridge supports only one battery, we use an average of the levels for the
|
||||||
|
battery level.
|
||||||
|
If one of the batteries is recharging, we consider the battery as recharging.
|
||||||
|
*/
|
||||||
|
|
||||||
|
GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo();
|
||||||
|
evBattery.level = 0;
|
||||||
|
boolean batteryCharging = false;
|
||||||
|
|
||||||
|
int numBatteries = payload[0];
|
||||||
|
for (int i = 0; i < numBatteries; i++) {
|
||||||
|
evBattery.level += (short) ((payload[2 + 2 * i] & MASK_BATTERY) / numBatteries);
|
||||||
|
if (!batteryCharging)
|
||||||
|
batteryCharging = ((payload[2 + 2 * i]) & MASK_BATTERY_CHARGING) == 1;
|
||||||
|
//LOG.debug("single battery level: " + hexdump(payload, 2+2*i,1) +"-"+ ((payload[2+2*i] & 0xff))+":" + evBattery.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
evBattery.state = BatteryState.UNKNOWN;
|
||||||
|
evBattery.state = batteryCharging ? BatteryState.BATTERY_CHARGING : evBattery.state;
|
||||||
|
|
||||||
|
return new GBDeviceEvent[]{evBattery};
|
||||||
|
}
|
||||||
|
|
||||||
|
private short getRequestCommand(short command) {
|
||||||
|
return (short) (command | MASK_REQUEST_CMD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOk(short control) {
|
||||||
|
return (control & MASK_RSP_CODE) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSupportedDevice(short control) {
|
||||||
|
return getDeviceType(control) == CONTROL_DEVICE_TYPE_TWS_HEADSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte getDeviceType(short control) {
|
||||||
|
return (byte) ((control & MASK_DEVICE_TYPE) >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected NothingProtocol(GBDevice device) {
|
||||||
|
super(device);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,24 @@ public class CheckSums {
|
|||||||
crc &= 0xffff;
|
crc &= 0xffff;
|
||||||
return crc;
|
return crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getCRC16ansi(byte[] seq) {
|
||||||
|
int crc = 0xffff;
|
||||||
|
int polynomial = 0xA001;
|
||||||
|
|
||||||
|
for (int i = 0; i < seq.length; i++) {
|
||||||
|
crc ^= seq[i] & 0xFF;
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
if ((crc & 1) != 0) {
|
||||||
|
crc = (crc >>> 1) ^ polynomial;
|
||||||
|
} else {
|
||||||
|
crc = crc >>> 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
public static int getCRC32(byte[] seq) {
|
public static int getCRC32(byte[] seq) {
|
||||||
CRC32 crc = new CRC32();
|
CRC32 crc = new CRC32();
|
||||||
|
@ -97,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
||||||
@ -306,6 +307,7 @@ public class DeviceHelper {
|
|||||||
result.add(new UM25Coordinator());
|
result.add(new UM25Coordinator());
|
||||||
result.add(new DomyosT540Cooridnator());
|
result.add(new DomyosT540Cooridnator());
|
||||||
result.add(new FitProDeviceCoordinator());
|
result.add(new FitProDeviceCoordinator());
|
||||||
|
result.add(new Ear1Coordinator());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
app/src/main/res/drawable/ic_device_nothingear.xml
Normal file
26
app/src/main/res/drawable/ic_device_nothingear.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="45sp"
|
||||||
|
android:height="45sp"
|
||||||
|
android:viewportWidth="30"
|
||||||
|
android:viewportHeight="30">
|
||||||
|
<path
|
||||||
|
android:fillColor="#2196f3"
|
||||||
|
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
|
||||||
|
android:strokeWidth="3.57115173" />
|
||||||
|
<path
|
||||||
|
android:pathData="m11.248,18.9974a2.6708,2.6529 90,0 1,-2.6529 -2.6708l0,-3.6234a5.5464,5.5093 89.9997,0 1,-3.3073 -1.6559,0.8903 0.8843,89.9998 0,1 -0.2299,-0.6054l0,-3.3652a0.8903,0.8843 89.9998,0 1,0.2299 -0.6054,5.6977 5.6596,90.0002 0,1 4.1916,-1.7182l0.1945,0a4.4513,4.4216 89.9998,0 1,4.227 4.4513l0,7.1222a2.6708,2.6529 90,0 1,-2.6529 2.6708zM6.8264,10.0947a4.113,4.0855 90.0003,0 0,2.6529 0.8903,0.8903 0.8843,89.9998 0,1 0.8843,0.8903l0,4.4513a0.8903,0.8843 90,0 0,1.7686 0L12.1323,9.2044a2.6708,2.6529 90,0 0,-2.5114 -2.6708l-0.1415,0a4.113,4.0855 90.0003,0 0,-2.6529 0.8903z"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.3225,24.339A2.6708,2.6529 90,0 1,15.6695 21.6682L15.6695,14.546a4.4513,4.4216 89.9998,0 1,4.227 -4.4513l0.1945,0a5.6977,5.6596 90.0002,0 1,4.1916 1.7182,0.8903 0.8843,89.9998 0,1 0.2299,0.6054l0,3.3652a0.8903,0.8843 89.9998,0 1,-0.2299 0.6054,5.5464 5.5093,89.9997 0,1 -3.3073,1.6559l0,3.6234a2.6708,2.6529 90,0 1,-2.6529 2.6708zM20.0911,11.8752l-0.1415,0a2.6708,2.6529 90,0 0,-2.5114 2.6708l0,7.1222a0.8903,0.8843 90.0003,0 0,1.7686 0l0,-4.4513a0.8903,0.8843 89.9998,0 1,0.8843 -0.8903,4.113 4.0855,90.0003 0,0 2.6529,-0.8903L22.744,12.7655a4.113,4.0855 90.0003,0 0,-2.6529 -0.8903z"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.595,8.7593m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.9754,14.1009m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#aa0000"/>
|
||||||
|
</vector>
|
26
app/src/main/res/drawable/ic_device_nothingear_disabled.xml
Normal file
26
app/src/main/res/drawable/ic_device_nothingear_disabled.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="45sp"
|
||||||
|
android:height="45sp"
|
||||||
|
android:viewportWidth="30"
|
||||||
|
android:viewportHeight="30">
|
||||||
|
<path
|
||||||
|
android:fillColor="#8a8a8a"
|
||||||
|
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
|
||||||
|
android:strokeWidth="3.57115173" />
|
||||||
|
<path
|
||||||
|
android:pathData="m11.248,18.9974a2.6708,2.6529 90,0 1,-2.6529 -2.6708l0,-3.6234a5.5464,5.5093 89.9997,0 1,-3.3073 -1.6559,0.8903 0.8843,89.9998 0,1 -0.2299,-0.6054l0,-3.3652a0.8903,0.8843 89.9998,0 1,0.2299 -0.6054,5.6977 5.6596,90.0002 0,1 4.1916,-1.7182l0.1945,0a4.4513,4.4216 89.9998,0 1,4.227 4.4513l0,7.1222a2.6708,2.6529 90,0 1,-2.6529 2.6708zM6.8264,10.0947a4.113,4.0855 90.0003,0 0,2.6529 0.8903,0.8903 0.8843,89.9998 0,1 0.8843,0.8903l0,4.4513a0.8903,0.8843 90,0 0,1.7686 0L12.1323,9.2044a2.6708,2.6529 90,0 0,-2.5114 -2.6708l-0.1415,0a4.113,4.0855 90.0003,0 0,-2.6529 0.8903z"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.3225,24.339A2.6708,2.6529 90,0 1,15.6695 21.6682L15.6695,14.546a4.4513,4.4216 89.9998,0 1,4.227 -4.4513l0.1945,0a5.6977,5.6596 90.0002,0 1,4.1916 1.7182,0.8903 0.8843,89.9998 0,1 0.2299,0.6054l0,3.3652a0.8903,0.8843 89.9998,0 1,-0.2299 0.6054,5.5464 5.5093,89.9997 0,1 -3.3073,1.6559l0,3.6234a2.6708,2.6529 90,0 1,-2.6529 2.6708zM20.0911,11.8752l-0.1415,0a2.6708,2.6529 90,0 0,-2.5114 2.6708l0,7.1222a0.8903,0.8843 90.0003,0 0,1.7686 0l0,-4.4513a0.8903,0.8843 89.9998,0 1,0.8843 -0.8903,4.113 4.0855,90.0003 0,0 2.6529,-0.8903L22.744,12.7655a4.113,4.0855 90.0003,0 0,-2.6529 -0.8903z"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.595,8.7593m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.9754,14.1009m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
|
||||||
|
android:strokeWidth="0.8872858"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
@ -1775,5 +1775,11 @@
|
|||||||
<item>2</item>
|
<item>2</item>
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="nothing_ear1_audio_mode">
|
||||||
|
<item>anc</item>
|
||||||
|
<item>transparency</item>
|
||||||
|
<item>off</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
@ -1271,4 +1271,5 @@
|
|||||||
<string name="prefs_autoheartrate_measurement">Automatic Heart Rate measurements</string>
|
<string name="prefs_autoheartrate_measurement">Automatic Heart Rate measurements</string>
|
||||||
<string name="prefs_autoheartrate_sleep">Take measurements during sleep</string>
|
<string name="prefs_autoheartrate_sleep">Take measurements during sleep</string>
|
||||||
<string name="prefs_autoheartrate_interval">Frequency of measurements</string>
|
<string name="prefs_autoheartrate_interval">Frequency of measurements</string>
|
||||||
|
<string name="devicetype_nothingear1">Nothing Ear (1)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
16
app/src/main/res/xml/devicesettings_nothing_ear1.xml
Normal file
16
app/src/main/res/xml/devicesettings_nothing_ear1.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:icon="@drawable/ic_extension"
|
||||||
|
android:key="pref_nothing_inear_detection"
|
||||||
|
android:summary="Play/pause the music depending if you wear the earbuds"
|
||||||
|
android:title="In-Ear detection" />
|
||||||
|
<ListPreference
|
||||||
|
android:icon="@drawable/ic_extension"
|
||||||
|
android:entryValues="@array/nothing_ear1_audio_mode"
|
||||||
|
android:entries="@array/nothing_ear1_audio_mode"
|
||||||
|
android:key="pref_nothing_audiomode"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="Audio mode" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user