1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-18 11:00:09 +02: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:
Andreas Shimokawa 2023-12-06 15:56:18 +01:00
parent 130e2ab85c
commit b44b0fec7e
6 changed files with 427 additions and 0 deletions

View File

@ -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,
};
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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>