From a7f42f0c4f4f8ec651249f58fd8f290c7f290620 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 30 Sep 2021 22:40:18 +0200 Subject: [PATCH] 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 Co-committed-by: daniele --- .../DeviceSettingsPreferenceConst.java | 3 + .../DeviceSpecificSettingsFragment.java | 5 + .../devices/nothing/Ear1Coordinator.java | 130 ++++++++++ .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceSupportFactory.java | 4 + .../service/devices/nothing/Ear1Support.java | 89 +++++++ .../devices/nothing/NothingIOThread.java | 45 ++++ .../devices/nothing/NothingProtocol.java | 235 ++++++++++++++++++ .../gadgetbridge/util/CheckSums.java | 18 ++ .../gadgetbridge/util/DeviceHelper.java | 2 + .../res/drawable/ic_device_nothingear.xml | 26 ++ .../ic_device_nothingear_disabled.xml | 26 ++ app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 1 + .../res/xml/devicesettings_nothing_ear1.xml | 16 ++ 15 files changed, 607 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/Ear1Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/Ear1Support.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingIOThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocol.java create mode 100644 app/src/main/res/drawable/ic_device_nothingear.xml create mode 100644 app/src/main/res/drawable/ic_device_nothingear_disabled.xml create mode 100644 app/src/main/res/xml/devicesettings_nothing_ear1.xml 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 8e9e557ee..d13f86859 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 @@ -95,6 +95,9 @@ public class DeviceSettingsPreferenceConst { 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_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_AUTH_KEY = "authkey"; } 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 197955fa8..effd93614 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 @@ -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_WEARLOCATION; 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_DEVICE_ACTION_FELL_SLEEP_BROADCAST; 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_SMART_INTERVAL); + addPreferenceHandlerFor(PREF_NOTHING_EAR1_INEAR); + addPreferenceHandlerFor(PREF_NOTHING_EAR1_AUDIOMODE); + String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/Ear1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/Ear1Coordinator.java new file mode 100644 index 000000000..b63c97ff3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/Ear1Coordinator.java @@ -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 getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider 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 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 + }; + } + +} 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 7ee8b3f4f..9aa428005 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -100,6 +100,7 @@ public enum DeviceType { 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), 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); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 1daf0e588..39572d05e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -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.miscale2.MiScale2DeviceSupport; 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.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport; @@ -358,6 +359,9 @@ public class DeviceSupportFactory { case FITPRO: deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case NOTHING_EAR1: + deviceSupport = new ServiceDeviceSupport(new Ear1Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/Ear1Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/Ear1Support.java new file mode 100644 index 000000000..a21d5420e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/Ear1Support.java @@ -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 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()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingIOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingIOThread.java new file mode 100644 index 000000000..0c803be4e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingIOThread.java @@ -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); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocol.java new file mode 100644 index 000000000..5567adeaf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocol.java @@ -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); + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java index b47599bcb..28c91189c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java @@ -57,6 +57,24 @@ public class CheckSums { crc &= 0xffff; 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) { CRC32 crc = new CRC32(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 2ec3a4570..431df5ccf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -97,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator; 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.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator; @@ -306,6 +307,7 @@ public class DeviceHelper { result.add(new UM25Coordinator()); result.add(new DomyosT540Cooridnator()); result.add(new FitProDeviceCoordinator()); + result.add(new Ear1Coordinator()); return result; } diff --git a/app/src/main/res/drawable/ic_device_nothingear.xml b/app/src/main/res/drawable/ic_device_nothingear.xml new file mode 100644 index 000000000..8b32ce55c --- /dev/null +++ b/app/src/main/res/drawable/ic_device_nothingear.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_device_nothingear_disabled.xml b/app/src/main/res/drawable/ic_device_nothingear_disabled.xml new file mode 100644 index 000000000..88c68f63c --- /dev/null +++ b/app/src/main/res/drawable/ic_device_nothingear_disabled.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 47708b399..b3749e334 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1775,5 +1775,11 @@ 2 3 + + + anc + transparency + off + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ba88df0c..10c82c61f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1271,4 +1271,5 @@ Automatic Heart Rate measurements Take measurements during sleep Frequency of measurements + Nothing Ear (1) diff --git a/app/src/main/res/xml/devicesettings_nothing_ear1.xml b/app/src/main/res/xml/devicesettings_nothing_ear1.xml new file mode 100644 index 000000000..2e1a3e84b --- /dev/null +++ b/app/src/main/res/xml/devicesettings_nothing_ear1.xml @@ -0,0 +1,16 @@ + + + + +