mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-11 12:39:26 +01:00
Divoom Pixoo: Initial experimental support
Works: - notifications - call notification - set time - setting brightness - setting 24h/12h format - sending weather Note - this is implemented using using the classic bluetooth serial protocol, the device can do BLE, but I don't know how to use it, as I did not have the offical app to sniff. - The information about the protocol comes from here https://github.com/jfroehlich/node-p1x3lramen/blob/main/source/devices/pixoo.js TODO: - Enable beep? Possible? I heard it beep once at least when switching it on - Getting out of factory mode? Why does it always play animations even when I switch to the clock? - Implement switching modes (can be done with the button) - Implement sending own images and animations - Firmware update? - ...
This commit is contained in:
parent
130e2ab85c
commit
b44b0fec7e
@ -0,0 +1,82 @@
|
||||
/* Copyright (C) 2023 Andreas Shimokawa
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.divoom;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.divoom.PixooSupport;
|
||||
|
||||
public class PixooCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Pixoo");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Divoom";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return PixooSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_pixoo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_lovetoy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_lovetoy_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_screen_brightness,
|
||||
R.xml.devicesettings_timeformat,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900Devi
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
|
||||
@ -288,6 +289,7 @@ public enum DeviceType {
|
||||
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
|
||||
SONY_WENA_3(SonyWena3Coordinator.class),
|
||||
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
|
||||
PIXOO(PixooCoordinator.class),
|
||||
TEST(TestDeviceCoordinator.class);
|
||||
|
||||
private DeviceCoordinator coordinator;
|
||||
|
@ -0,0 +1,57 @@
|
||||
/* Copyright (C) 2023 Andreas Shimokawa
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.divoom;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
|
||||
|
||||
public class PixooIOThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PixooIOThread.class);
|
||||
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
setUpdateState(GBDevice.State.INITIALIZED);
|
||||
}
|
||||
|
||||
public PixooIOThread(GBDevice device, Context context, PixooProtocol deviceProtocol,
|
||||
PixooSupport PixooSupport, BluetoothAdapter bluetoothAdapter) {
|
||||
super(device, context, deviceProtocol, PixooSupport, bluetoothAdapter);
|
||||
}
|
||||
|
||||
@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,220 @@
|
||||
/* Copyright (C) 2023 Andreas Shimokawa
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.divoom;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import lineageos.weather.util.WeatherUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class PixooProtocol extends GBDeviceProtocol {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PixooProtocol.class);
|
||||
|
||||
private boolean isFirstExchange = true;
|
||||
|
||||
protected PixooProtocol(GBDevice device) {
|
||||
super(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||
List<GBDeviceEvent> devEvts = new ArrayList<>();
|
||||
|
||||
if (isFirstExchange) {
|
||||
isFirstExchange = false;
|
||||
devEvts.add(new GBDeviceEventVersionInfo()); //TODO: this is a weird hack to make the DBHelper happy. Replace with proper firmware detection
|
||||
}
|
||||
|
||||
ByteBuffer incoming = ByteBuffer.wrap(responseData);
|
||||
incoming.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
return devEvts.toArray(new GBDeviceEvent[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSendConfiguration(String config) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS:
|
||||
byte brightness = (byte) prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS, 50);
|
||||
LOG.debug("setting brightness to " + brightness);
|
||||
return encodeProtocol(new byte[]{
|
||||
0x74,
|
||||
brightness
|
||||
});
|
||||
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
|
||||
final GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())));
|
||||
final String timeFormat = gbPrefs.getTimeFormat();
|
||||
final boolean is24hour = DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H.equals(timeFormat);
|
||||
|
||||
return encodeProtocol(new byte[]{
|
||||
0x2d,
|
||||
(byte) (is24hour ? 1 : 0),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSetCallState(String number, String name, int command) {
|
||||
if (command == CallSpec.CALL_OUTGOING) {
|
||||
return null;
|
||||
}
|
||||
return encodeProtocol(new byte[]{
|
||||
0x50,
|
||||
(byte) (command == CallSpec.CALL_INCOMING ? 5 : 6),
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeNotification(NotificationSpec notificationSpec) {
|
||||
byte iconID;
|
||||
switch (notificationSpec.type) {
|
||||
case KAKAO_TALK:
|
||||
iconID = 0;
|
||||
break;
|
||||
case INSTAGRAM:
|
||||
iconID = 1;
|
||||
break;
|
||||
case SNAPCHAT:
|
||||
iconID = 2;
|
||||
break;
|
||||
case FACEBOOK:
|
||||
iconID = 3;
|
||||
break;
|
||||
case TWITTER:
|
||||
iconID = 4;
|
||||
break;
|
||||
case WHATSAPP:
|
||||
case GENERIC_SMS:
|
||||
iconID = 8;
|
||||
break;
|
||||
case SKYPE:
|
||||
iconID = 9;
|
||||
break;
|
||||
case LINE:
|
||||
iconID = 10;
|
||||
break;
|
||||
case WECHAT:
|
||||
iconID = 11;
|
||||
break;
|
||||
case VIBER:
|
||||
iconID = 12;
|
||||
break;
|
||||
case GENERIC_ALARM_CLOCK:
|
||||
iconID = 17;
|
||||
break;
|
||||
default:
|
||||
iconID = 0x20;
|
||||
}
|
||||
|
||||
return encodeProtocol(new byte[]{
|
||||
0x50,
|
||||
iconID,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSetTime() {
|
||||
Calendar now = BLETypeConversions.createCalendar();
|
||||
return encodeProtocol(new byte[]{
|
||||
0x18,
|
||||
(byte) (now.get(Calendar.YEAR) % 100),
|
||||
(byte) (now.get(Calendar.YEAR) / 100),
|
||||
(byte) (now.get(Calendar.MONTH) + 1),
|
||||
(byte) now.get(Calendar.DAY_OF_MONTH),
|
||||
(byte) now.get(Calendar.HOUR_OF_DAY),
|
||||
(byte) now.get(Calendar.MINUTE),
|
||||
(byte) now.get(Calendar.SECOND)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSendWeather(WeatherSpec weatherSpec) {
|
||||
byte pixooWeatherCode = 0;
|
||||
if (weatherSpec.currentConditionCode >= 200 && weatherSpec.currentConditionCode <= 299) {
|
||||
pixooWeatherCode = 5;
|
||||
} else if (weatherSpec.currentConditionCode >= 300 && weatherSpec.currentConditionCode < 600) {
|
||||
pixooWeatherCode = 6;
|
||||
} else if (weatherSpec.currentConditionCode >= 600 && weatherSpec.currentConditionCode < 700) {
|
||||
pixooWeatherCode = 8;
|
||||
} else if (weatherSpec.currentConditionCode >= 700 && weatherSpec.currentConditionCode < 800) {
|
||||
pixooWeatherCode = 9;
|
||||
} else if (weatherSpec.currentConditionCode == 800) {
|
||||
pixooWeatherCode = 1;
|
||||
} else if (weatherSpec.currentConditionCode >= 801 && weatherSpec.currentConditionCode <= 804) {
|
||||
pixooWeatherCode = 3;
|
||||
}
|
||||
|
||||
byte temp = (byte) (weatherSpec.currentTemp - 273);
|
||||
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
||||
if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
|
||||
temp = (byte) WeatherUtils.celsiusToFahrenheit(temp);
|
||||
}
|
||||
|
||||
return encodeProtocol(new byte[]{
|
||||
0x5f,
|
||||
temp,
|
||||
pixooWeatherCode,
|
||||
});
|
||||
}
|
||||
|
||||
byte[] encodeProtocol(byte[] payload) {
|
||||
|
||||
ByteBuffer msgBuf = ByteBuffer.allocate(6 + payload.length);
|
||||
msgBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
msgBuf.put((byte) 0x01);
|
||||
msgBuf.putShort((short) (payload.length + 2));
|
||||
msgBuf.put(payload);
|
||||
short crc = (short) (((payload.length + 2) & 0xff) + ((payload.length + 2) >> 8));
|
||||
for (byte b : payload) {
|
||||
crc += b;
|
||||
}
|
||||
msgBuf.putShort(crc);
|
||||
msgBuf.put((byte) 0x02);
|
||||
return msgBuf.array();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/* Copyright (C) 2023 Andreas Shimokawa
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.divoom;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class PixooSupport extends AbstractSerialDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PixooSupport.class);
|
||||
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
super.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connect() {
|
||||
getDeviceIOThread().start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized PixooIOThread getDeviceIOThread() {
|
||||
return (PixooIOThread) super.getDeviceIOThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
return new PixooProtocol(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new PixooIOThread(getDevice(), getContext(), (PixooProtocol) getDeviceProtocol(),
|
||||
PixooSupport.this, getBluetoothAdapter());
|
||||
}
|
||||
}
|
@ -2432,4 +2432,5 @@
|
||||
<string name="pref_sleep_mode_schedule_summary">Send a reminder and enter sleep mode at bedtime. At the scheduled wake-up time, the wake-up alarm will sound.</string>
|
||||
<string name="devicetype_xiaomi_watch_s1_active">Xiaomi Watch S1 Active</string>
|
||||
<string name="devicetype_mi_watch_color_sport">Mi Watch Color Sport</string>
|
||||
<string name="devicetype_pixoo">Pixoo</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user