/* Copyright (C) 2024 Severin von Wnuck-Lipinski
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
public class SoundcoreMotion300Protocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreMotion300Protocol.class);
// Some of these commands are not used right now, they serve as documentation
private static final short CMD_GET_DEVICE_INFO = (short)0x0101;
private static final short CMD_GET_LDAC_MODE = (short)0x7f01;
private static final short CMD_GET_BUTTON_BRIGHTNESS = (short)0x9310;
private static final short CMD_GET_EQUALIZER = (short)0x8902;
private static final short CMD_GET_CURRENT_DIRECTION = (short)0x8c02;
private static final short CMD_SET_VOICE_PROMPTS = (short)0x9001;
private static final short CMD_SET_BUTTON_BRIGHTNESS = (short)0x9210;
private static final short CMD_SET_AUTO_POWER_OFF = (short)0x8601;
private static final short CMD_SET_LDAC_MODE = (short)0xff01;
private static final short CMD_SET_ADAPTIVE_DIRECTION = (short)0x8a02;
private static final short CMD_SET_EQUALIZER_PRESET = (short)0x8b02;
private static final short CMD_SET_EQUALIZER_CUSTOM = (short)0x8d02;
private static final short CMD_NOTIFY_BATTERY_INFO = (short)0x0301;
private static final short CMD_NOTIFY_CHARGING_INFO = (short)0x0401;
private static final short CMD_NOTIFY_VOLUME_INFO = (short)0x0901;
private static final short CMD_NOTIFY_PLAYBACK_INFO = (short)0x2101;
private static final short CMD_NOTIFY_BASS_MODE = (short)0x8e02;
private static final short CMD_POWER_OFF = (short)0x8901;
public static final String[] EQUALIZER_PREFS_FREQ = new String[] {
PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ,
PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ
};
public static final String[] EQUALIZER_PREFS_VALUE = new String[] {
PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE,
PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE
};
private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
protected SoundcoreMotion300Protocol(GBDevice device) {
super(device);
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] data) {
ByteBuffer buf = ByteBuffer.wrap(data);
SoundcorePacket packet = SoundcorePacket.decode(buf);
if (packet == null)
return null;
short cmd = packet.getCommand();
byte[] payload = packet.getPayload();
switch (cmd) {
case CMD_GET_DEVICE_INFO:
return handlePacketDeviceInfo(payload);
case CMD_GET_LDAC_MODE:
return handlePacketLdacMode(payload);
case CMD_GET_BUTTON_BRIGHTNESS:
return handlePacketButtonBrightness(payload);
case CMD_GET_EQUALIZER:
return handlePacketEqualizer(payload);
case CMD_SET_ADAPTIVE_DIRECTION:
return handlePacketAdaptiveDirection(payload);
case CMD_NOTIFY_BATTERY_INFO:
return handlePacketBatteryInfo(payload);
case CMD_NOTIFY_CHARGING_INFO:
return handlePacketChargingInfo(payload);
}
return null;
}
private GBDeviceEvent[] handlePacketDeviceInfo(byte[] payload) {
if (payload.length != 29)
return null;
ByteBuffer buf = ByteBuffer.wrap(payload);
byte volume = buf.get();
byte batteryLevel = buf.get();
byte batteryCharging = buf.get();
byte currentlyPlaying = buf.get();
byte voicePrompts = buf.get();
byte autoPowerOffEnabled = buf.get();
byte autoPowerOffDuration = buf.get();
byte[] firmwareBytes = new byte[5];
byte[] serialBytes = new byte[17];
buf.get(firmwareBytes);
buf.get(serialBytes);
String fwVersion = new String(firmwareBytes, StandardCharsets.UTF_8);
String serialNumber = new String(serialBytes, StandardCharsets.UTF_8);
GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
versionInfo.fwVersion = fwVersion;
batteryInfo.state = batteryCharging == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING;
batteryInfo.level = batteryLevel * 20;
LOG.debug(
"Device info: volume={}, battery={}, charging={}, playing={}, prompts={}, autoOff={}, autoOffDuration={}",
volume, batteryLevel, batteryCharging, currentlyPlaying, voicePrompts, autoPowerOffEnabled, autoPowerOffDuration
);
return new GBDeviceEvent[] {
versionInfo,
batteryInfo,
new GBDeviceEventUpdateDeviceInfo("SERIAL: ", serialNumber),
new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_VOICE_PROMPTS, voicePrompts != (byte)0x00),
new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_AUTO_POWER_OFF, String.valueOf(autoPowerOffDuration + 1)),
new GBDeviceEventSendBytes(encodeRequest(CMD_GET_LDAC_MODE)),
};
}
private GBDeviceEvent[] handlePacketLdacMode(byte[] payload) {
if (payload.length != 1)
return null;
return new GBDeviceEvent[] {
new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_LDAC_MODE, payload[0] != (byte)0x00),
new GBDeviceEventSendBytes(encodeRequest(CMD_GET_BUTTON_BRIGHTNESS)),
};
}
private GBDeviceEvent[] handlePacketButtonBrightness(byte[] payload) {
if (payload.length != 1)
return null;
return new GBDeviceEvent[] {
new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_BUTTON_BRIGHTNESS, String.valueOf(payload[0])),
new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)),
};
}
private GBDeviceEvent[] handlePacketEqualizer(byte[] payload) {
if (payload.length != 57)
return null;
// Get direction chosen in custom equalizer preferences
Prefs prefs = getDevicePrefs();
int equalizerDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"));
// Equalizer preset might be larger than 127 -> convert to unsigned int
ByteBuffer buf = ByteBuffer.wrap(payload);
byte adaptiveDirection = buf.get();
byte currentDirection = buf.get();
int equalizerPreset = Byte.toUnsignedInt(buf.get());
byte[] equalizer = new byte[18];
// Choose one of the three custom equalizer configurations
buf.position(buf.position() + equalizerDirection * equalizer.length);
buf.get(equalizer);
LOG.debug(
"Equalizer: adaptiveDirection={}, direction={}, preset={}",
adaptiveDirection, currentDirection, equalizerPreset
);
Map newPrefs = equalizerToPrefs(equalizer);
newPrefs.put(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, adaptiveDirection != (byte)0x00);
newPrefs.put(PREF_SOUNDCORE_EQUALIZER_PRESET, String.valueOf(equalizerPreset));
return new GBDeviceEvent[] {
new GBDeviceEventUpdatePreferences(newPrefs),
new GBDeviceEventUpdateDeviceState(GBDevice.State.INITIALIZED),
};
}
private GBDeviceEvent[] handlePacketAdaptiveDirection(byte[] payload) {
if (payload.length != 0)
return null;
Prefs prefs = getDevicePrefs();
// Ignore if adaptive direction is enabled
if (prefs.getBoolean(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, true))
return null;
// Set equalizer direction to "standing" and get current configuration
return new GBDeviceEvent[] {
new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"),
new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)),
};
}
private GBDeviceEvent[] handlePacketBatteryInfo(byte[] payload) {
if (payload.length != 1)
return null;
batteryInfo.level = payload[0] * 20;
return new GBDeviceEvent[] { batteryInfo };
}
private GBDeviceEvent[] handlePacketChargingInfo(byte[] payload) {
if (payload.length != 1)
return null;
batteryInfo.state = payload[0] == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING;
return new GBDeviceEvent[] { batteryInfo };
}
@Override
public byte[] encodeSendConfiguration(String config) {
Prefs prefs = getDevicePrefs();
switch (config) {
case PREF_SOUNDCORE_VOICE_PROMPTS:
return encodeSetBoolean(prefs, PREF_SOUNDCORE_VOICE_PROMPTS, CMD_SET_VOICE_PROMPTS);
case PREF_SOUNDCORE_BUTTON_BRIGHTNESS:
return encodeSetByte(prefs, PREF_SOUNDCORE_BUTTON_BRIGHTNESS, CMD_SET_BUTTON_BRIGHTNESS);
case PREF_SOUNDCORE_AUTO_POWER_OFF:
return encodeSetAutoPowerOff(prefs);
case PREF_SOUNDCORE_LDAC_MODE:
return encodeSetBoolean(prefs, PREF_SOUNDCORE_LDAC_MODE, CMD_SET_LDAC_MODE);
case PREF_SOUNDCORE_ADAPTIVE_DIRECTION:
return encodeSetBoolean(prefs, PREF_SOUNDCORE_ADAPTIVE_DIRECTION, CMD_SET_ADAPTIVE_DIRECTION);
case PREF_SOUNDCORE_EQUALIZER_PRESET:
return encodeSetByte(prefs, PREF_SOUNDCORE_EQUALIZER_PRESET, CMD_SET_EQUALIZER_PRESET);
case PREF_SOUNDCORE_EQUALIZER_DIRECTION:
return encodeRequest(CMD_GET_EQUALIZER);
case PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE:
case PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ:
case PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE:
return encodeSetEqualizerCustom(prefs);
}
return super.encodeSendConfiguration(config);
}
private byte[] encodeRequest(short cmd) {
return new SoundcorePacket(cmd).encode();
}
public byte[] encodeGetDeviceInfo() {
return encodeRequest(CMD_GET_DEVICE_INFO);
}
@Override
public byte[] encodePowerOff() {
return encodeRequest(CMD_POWER_OFF);
}
private byte[] encodeSetBoolean(Prefs prefs, String pref, short cmd) {
boolean enabled = prefs.getBoolean(pref, true);
byte[] payload = new byte[] { enabled ? (byte)0x01 : (byte)0x00 };
return new SoundcorePacket(cmd, payload).encode();
}
private byte[] encodeSetByte(Prefs prefs, String pref, short cmd) {
byte value = (byte)Integer.parseInt(prefs.getString(pref, "0"));
byte[] payload = new byte[] { value };
return new SoundcorePacket(cmd, payload).encode();
}
private byte[] encodeSetAutoPowerOff(Prefs prefs) {
byte duration = (byte)Integer.parseInt(prefs.getString(PREF_SOUNDCORE_AUTO_POWER_OFF, "2"));
byte[] payload;
if (duration > 0)
payload = new byte[] { (byte)0x01, (byte)(duration - 1) };
else
payload = new byte[] { (byte)0x00, (byte)0x00 };
return new SoundcorePacket(CMD_SET_AUTO_POWER_OFF, payload).encode();
}
private byte[] encodeSetEqualizerCustom(Prefs prefs) {
ByteBuffer buf = ByteBuffer.allocate(21);
int eqDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"));
byte[] equalizer = equalizerFromPrefs(prefs);
// Bit combination of the equalizer directions that should be changed
buf.put((byte)(1 << eqDirection));
buf.put((byte)0x01);
buf.put((byte)0xff);
buf.put(equalizer);
return new SoundcorePacket(CMD_SET_EQUALIZER_CUSTOM, buf.array()).encode();
}
private Map equalizerToPrefs(byte[] equalizer) {
Map prefs = new HashMap<>();
for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) {
// Equalizer values range from 60 to 180, with 120 as "neutral"
byte freq = equalizer[i * 2 + 1];
int value = Byte.toUnsignedInt(equalizer[i * 2]);
// Map equalizer value to preference range (0 to 120)
prefs.put(EQUALIZER_PREFS_FREQ[i], freq);
prefs.put(EQUALIZER_PREFS_VALUE[i], value - 60);
}
return prefs;
}
private byte[] equalizerFromPrefs(Prefs prefs) {
byte[] equalizer = new byte[EQUALIZER_PREFS_FREQ.length * 2];
for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) {
int freq = Integer.parseInt(prefs.getString(EQUALIZER_PREFS_FREQ[i], "0"));
int value = prefs.getInt(EQUALIZER_PREFS_VALUE[i], 60);
// Map equalizer values from 0 - 120 back to 60 - 180
equalizer[i * 2 + 1] = ((byte)freq);
equalizer[i * 2] = (byte)(value + 60);
}
return equalizer;
}
}