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 0505e4e66c..b15b209a77 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 @@ -374,6 +374,9 @@ public class DeviceSettingsPreferenceConst { 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_NOTHING_EAR1_ULTRA_BASS_ENABLED = "pref_nothing_ultra_bass_enabled"; + public static final String PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL = "pref_nothing_ultra_bass_level"; + public static final String PREF_HEADPHONES_EQUALIZER = "pref_headphones_equalizer"; public static final String PREF_HEADPHONES_LOW_LATENCY = "pref_headphones_low_latency"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/AbstractEarCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/AbstractEarCoordinator.java index 449ff140ec..638dc0345a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/AbstractEarCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/AbstractEarCoordinator.java @@ -18,6 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.devices.nothing; import androidx.annotation.NonNull; +import java.util.Collections; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; @@ -66,6 +69,12 @@ public abstract class AbstractEarCoordinator extends AbstractBLClassicDeviceCoor public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_ear1); + if (!getEqualizerPresets().isEmpty()) { + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_equalizer); + } + if (supportsUltraBass()) { + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_ultra_bass); + } if (supportsLowLatency()) { deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones_low_latency); } @@ -100,4 +109,12 @@ public abstract class AbstractEarCoordinator extends AbstractBLClassicDeviceCoor public boolean supportsLowLatency() { return false; } + + public List getEqualizerPresets() { + return Collections.emptyList(); + } + + public boolean supportsUltraBass() { + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/CmfBuds2aCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/CmfBuds2aCoordinator.java index 7617c27551..b5f20183e0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/CmfBuds2aCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/CmfBuds2aCoordinator.java @@ -16,6 +16,9 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.nothing; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.R; @@ -70,4 +73,21 @@ public class CmfBuds2aCoordinator extends AbstractEarCoordinator { public boolean supportsLowLatency() { return true; } + + @Override + public List getEqualizerPresets() { + return Arrays.asList( + NothingEqualizer.POP, + NothingEqualizer.ROCK, + NothingEqualizer.ELECTRONIC, + NothingEqualizer.ENHANCE_VOCALS, + NothingEqualizer.CLASSICAL, + NothingEqualizer.DIRAC + ); + } + + @Override + public boolean supportsUltraBass() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/EarSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/EarSettingsCustomizer.java index a797e64271..7cea16c9e9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/EarSettingsCustomizer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/EarSettingsCustomizer.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.nothing; import android.os.Parcel; +import androidx.annotation.NonNull; import androidx.preference.ListPreference; import androidx.preference.Preference; @@ -32,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer { - public static final Creator CREATOR = new Creator() { + public static final Creator CREATOR = new Creator<>() { @Override public EarSettingsCustomizer createFromParcel(final Parcel in) { return new EarSettingsCustomizer(); @@ -88,6 +89,33 @@ public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer { ((ListPreference) audioModePref).setEntries(entries.toArray(new CharSequence[0])); ((ListPreference) audioModePref).setEntryValues(entryValues.toArray(new CharSequence[0])); } + + final Preference equalizerPref = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER); + if (equalizerPref instanceof ListPreference listPreference) { + final List supportedPresets = earCoordinator.getEqualizerPresets(); + if (supportedPresets.isEmpty()) { + listPreference.setVisible(false); + } else { + final CharSequence[] originalEntries = listPreference.getEntries(); + final CharSequence[] originalEntryValues = listPreference.getEntryValues(); + + final List entries = new ArrayList<>(); + final List entryValues = new ArrayList<>(); + + for (int i = 0; i < originalEntryValues.length; i++) { + final NothingEqualizer preset = NothingEqualizer.fromPreferenceValue(originalEntryValues[i].toString()); + if (preset == null || !supportedPresets.contains(preset)) { + continue; + } + + entries.add(originalEntries[i]); + entryValues.add(originalEntryValues[i]); + } + + listPreference.setEntries(entries.toArray(new CharSequence[0])); + listPreference.setEntryValues(entryValues.toArray(new CharSequence[0])); + } + } } @Override @@ -101,6 +129,6 @@ public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer { } @Override - public void writeToParcel(final Parcel dest, final int flags) { + public void writeToParcel(@NonNull final Parcel dest, final int flags) { } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/NothingEqualizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/NothingEqualizer.java new file mode 100644 index 0000000000..0dafd346db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nothing/NothingEqualizer.java @@ -0,0 +1,43 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.nothing; + +public enum NothingEqualizer { + ROCK((byte) 0x01), + ELECTRONIC((byte) 0x02), + POP((byte) 0x03), + ENHANCE_VOCALS((byte) 0x04), + CLASSICAL((byte) 0x05), + DIRAC((byte) 0x07), + ; + + private final byte code; + + NothingEqualizer(final byte code) { + this.code = code; + } + + public byte getCode() { + return this.code; + } + + public static NothingEqualizer fromCode(final byte code) { + for (NothingEqualizer value : NothingEqualizer.values()) { + if (value.getCode() == code) { + return value; + } + } + + return null; + } + + public static NothingEqualizer fromPreferenceValue(final String preferenceValue) { + if (preferenceValue == null) { + return null; + } + + try { + return NothingEqualizer.valueOf(preferenceValue); + } catch (final IllegalArgumentException e) { + return null; + } + } +} 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 index c8b6d88df9..59922e1268 100644 --- 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 @@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDevi import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.nothing.AbstractEarCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.nothing.NothingEqualizer; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneBTBRDeviceSupport; @@ -74,6 +75,12 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { sendCommand(builder, nothingProtocol.encodeFirmwareVersionReq()); sendCommand(builder, nothingProtocol.encodeBatteryStatusReq()); sendCommand(builder, nothingProtocol.encodeAudioModeStatusReq()); + if (!getCoordinator().getEqualizerPresets().isEmpty()) { + sendCommand(builder, nothingProtocol.encodeEqualizerStatusReq()); + } + if (getCoordinator().supportsUltraBass()) { + sendCommand(builder, nothingProtocol.encodeUltraBassStatusReq()); + } return builder; } @@ -135,6 +142,19 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { // OFF: 55 6001 40F0 0100 5C 02 28 9353 // OFF Response: 55 6001 4070 0000 5C FF FF95 break; + case DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER: + sendCommand("set equalizer", nothingProtocol.encodeEqualizer(prefs.getString(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER, NothingEqualizer.DIRAC.name()))); + break; + case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED: + case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL: + sendCommand( + "set ultra bass", + nothingProtocol.encodeUltraBass( + prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED, false), + prefs.getInt(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL, 2) + ) + ); + break; default: LOG.debug("CONFIG: " + config); break; @@ -166,6 +186,8 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { private static final short audio_mode_status = (short) 0xc01e; private static final short audio_mode_status2 = (short) 0xe003; private static final short low_latency = (short) 0xf040; + private static final short equalizer_status = (short) 0xc050; + private static final short ultra_bass_status = (short) 0xc04e; 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 @@ -175,6 +197,8 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { private static final short in_ear_detection = (short) 0xf004; private static final short in_ear_detection2 = (short) 0xc00e; private static final short audio_mode = (short) 0xf00f; + private static final short equalizer = (short) 0xf01d; + private static final short ultra_bass = (short) 0xf051; private final boolean incrementCounter; private int messageCounter = 0x00; @@ -259,6 +283,20 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { devEvts.add(handleInEarStatus(payload)); break; + case equalizer_status: + case equalizer: + if (payload.length > 0) { + devEvts.add(handleEqualizerStatus(payload)); + } + break; + + case ultra_bass_status: + case ultra_bass: + if (payload.length > 0) { + devEvts.add(handleUltraBassStatus(payload)); + } + break; + case unk_maybe_ack: LOG.debug("received ack"); break; @@ -320,6 +358,14 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { return encodeMessage((short) 0x120, audio_mode_status, new byte[]{}); } + byte[] encodeEqualizerStatusReq() { + return encodeMessage((short) 0x120, equalizer_status, new byte[]{}); + } + + byte[] encodeUltraBassStatusReq() { + return encodeMessage((short) 0x120, ultra_bass_status, new byte[]{}); + } + private GBDeviceEventVersionInfo handleFirmwareVersion(byte[] payload) { GBDeviceEventVersionInfo evt = new GBDeviceEventVersionInfo(); evt.fwVersion = new String(payload); @@ -351,6 +397,32 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { return preferencesEvent; } + private GBDeviceEventUpdatePreferences handleEqualizerStatus(final byte[] payload) { + final GBDeviceEventUpdatePreferences preferencesEvent = new GBDeviceEventUpdatePreferences(); + + final NothingEqualizer equalizerPreset = NothingEqualizer.fromCode(payload[0]); + if (equalizerPreset == null) { + LOG.warn("Unknown equalizer code: {}", payload[0] & 0xff); + return preferencesEvent; + } + + preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER, equalizerPreset.name()); + return preferencesEvent; + } + + private GBDeviceEventUpdatePreferences handleUltraBassStatus(final byte[] payload) { + final GBDeviceEventUpdatePreferences preferencesEvent = new GBDeviceEventUpdatePreferences(); + + preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED, payload[0] == 0x01); + + if (payload.length >= 2) { + final int level = Math.max(1, Math.min(5, ((payload[1] & 0xff) + 1) / 2)); + preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL, level); + } + + return preferencesEvent; + } + byte[] encodeInEarDetection(byte enabled) { return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled}); } @@ -372,6 +444,26 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport { return encodeMessage((short) 0x120, low_latency, new byte[]{payload}); } + byte[] encodeEqualizer(final String desired) { + NothingEqualizer preset = NothingEqualizer.DIRAC; + + final NothingEqualizer requested = NothingEqualizer.fromPreferenceValue(desired); + if (requested != null) { + preset = requested; + } else { + LOG.warn("Unknown equalizer preset: {}, using default", desired); + } + + return encodeMessage((short) 0x120, equalizer, new byte[]{preset.getCode(), 0x00}); + } + + byte[] encodeUltraBass(final boolean enabled, final int level) { + final int clampedLevel = Math.max(1, Math.min(5, level)); + final byte rawLevel = (byte) (clampedLevel * 2); + + return encodeMessage((short) 0x120, ultra_bass, new byte[]{(byte) (enabled ? 0x01 : 0x00), rawLevel}); + } + public byte[] encodeFindDevice(boolean start) { byte payload = (byte) (start ? 0x01 : 0x00); return encodeMessage((short) 0x120, find_device, new byte[]{payload}); diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2b6fee6c51..8dafe2a8dd 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3564,6 +3564,24 @@ off + + @string/nothing_equalizer_rock + @string/nothing_equalizer_electronic + @string/nothing_equalizer_pop + @string/nothing_equalizer_enhance_vocals + @string/nothing_equalizer_classical + @string/nothing_equalizer_dirac + + + + ROCK + ELECTRONIC + POP + ENHANCE_VOCALS + CLASSICAL + DIRAC + + @string/pref_title_notification_use_as_deny @string/pref_title_notification_use_as_allow diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9f98e2443..6d6a4ed01b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3177,6 +3177,15 @@ In-Ear detection Play calls through your earbuds when they are in your ears Audio mode + Ultra Bass + Boost low frequencies for a stronger bass response + Ultra Bass Level + Rock + Electronic + Pop + Enhance vocals + Classical + Dirac Equalizer Preset Normal Bass boost diff --git a/app/src/main/res/xml/devicesettings_nothing_equalizer.xml b/app/src/main/res/xml/devicesettings_nothing_equalizer.xml new file mode 100644 index 0000000000..6dca3d32f0 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_nothing_equalizer.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_nothing_ultra_bass.xml b/app/src/main/res/xml/devicesettings_nothing_ultra_bass.xml new file mode 100644 index 0000000000..ee31a09c46 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_nothing_ultra_bass.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocolTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocolTest.java new file mode 100644 index 0000000000..a3e2c9699f --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nothing/NothingProtocolTest.java @@ -0,0 +1,117 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.nothing.NothingEqualizer; +import nodomain.freeyourgadget.gadgetbridge.test.TestBase; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi; + +public class NothingProtocolTest extends TestBase { + @Test + public void testDecodeEqualizerAndUltraBassStatus() { + final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true); + + final GBDeviceEvent[] equalizerEvents = protocol.decodeResponse(GB.hexStringToByteArray("5560015040010060057f43")); + final GBDeviceEventUpdatePreferences equalizerUpdate = findEvent(equalizerEvents, GBDeviceEventUpdatePreferences.class); + Assert.assertNotNull(equalizerUpdate); + Assert.assertEquals(NothingEqualizer.CLASSICAL.name(), equalizerUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER)); + + final GBDeviceEvent[] ultraBassEvents = protocol.decodeResponse(GB.hexStringToByteArray("5560014e4002001b00028071")); + final GBDeviceEventUpdatePreferences ultraBassUpdate = findEvent(ultraBassEvents, GBDeviceEventUpdatePreferences.class); + Assert.assertNotNull(ultraBassUpdate); + Assert.assertEquals(false, ultraBassUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED)); + Assert.assertEquals(1, ultraBassUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL)); + } + + @Test + public void testEncodeEqualizerCommand() { + final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true); + + final byte[] packet = protocol.encodeEqualizer(NothingEqualizer.ELECTRONIC.name()); + final byte[] expected = GB.hexStringToByteArray("5560011df00200140200ebcd"); + assertMessageEquals(expected, packet); + } + + @Test + public void testEncodeUltraBassCommand() { + final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true); + + final byte[] packet = protocol.encodeUltraBass(true, 2); + final byte[] expected = GB.hexStringToByteArray("55600151f0020070010426e5"); + assertMessageEquals(expected, packet); + } + + private void assertMessageEquals(final byte[] expected, final byte[] actual) { + Assert.assertNotNull(expected); + Assert.assertNotNull(actual); + + Assert.assertTrue("message too short", expected.length >= 8 && actual.length >= 8); + Assert.assertEquals("sof differs", expected[0], actual[0]); + + final int expectedControl = readLeShort(expected, 1); + final int actualControl = readLeShort(actual, 1); + Assert.assertEquals("control differs", expectedControl, actualControl); + + final int expectedCommand = readLeShort(expected, 3); + final int actualCommand = readLeShort(actual, 3); + Assert.assertEquals("command differs", expectedCommand, actualCommand); + + final int expectedPayloadLength = readLeShort(expected, 5); + final int actualPayloadLength = readLeShort(actual, 5); + Assert.assertEquals("payload length field differs", expectedPayloadLength, actualPayloadLength); + + final boolean expectedCrc = (expectedControl & 0x20) != 0; + final boolean actualCrc = (actualControl & 0x20) != 0; + Assert.assertEquals("crc flag differs", expectedCrc, actualCrc); + + final int expectedTotalLength = 8 + expectedPayloadLength + (expectedCrc ? 2 : 0); + final int actualTotalLength = 8 + actualPayloadLength + (actualCrc ? 2 : 0); + Assert.assertEquals("expected message has invalid size", expectedTotalLength, expected.length); + Assert.assertEquals("actual message has invalid size", actualTotalLength, actual.length); + + // Counter byte at offset 7 is intentionally ignored. + Assert.assertArrayEquals( + "payload differs", + Arrays.copyOfRange(expected, 8, 8 + expectedPayloadLength), + Arrays.copyOfRange(actual, 8, 8 + actualPayloadLength) + ); + + assertCrcIsValid(expected, expectedCrc); + assertCrcIsValid(actual, actualCrc); + } + + private static int readLeShort(final byte[] msg, final int offset) { + return (msg[offset] & 0xff) | ((msg[offset + 1] & 0xff) << 8); + } + + private static void assertCrcIsValid(final byte[] msg, final boolean hasCrc) { + if (!hasCrc) { + return; + } + + final int payloadLength = readLeShort(msg, 5); + final int crcOffset = 8 + payloadLength; + final int expectedCrc = readLeShort(msg, crcOffset); + final int actualCrc = getCRC16ansi(Arrays.copyOf(msg, crcOffset)); + Assert.assertEquals("invalid crc", expectedCrc, actualCrc); + } + + @SuppressWarnings("unchecked") + private static T findEvent(final GBDeviceEvent[] events, final Class cls) { + for (GBDeviceEvent event : events) { + if (cls.isInstance(event)) { + return (T) event; + } + } + + return null; + } +}