diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/divoom/PixooCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/divoom/PixooCoordinator.java new file mode 100644 index 000000000..0617bc37b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/divoom/PixooCoordinator.java @@ -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 . */ +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 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, + }; + } + +} 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 f41b262b9..35639310e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooIOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooIOThread.java new file mode 100644 index 000000000..e4936ceae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooIOThread.java @@ -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 . */ + +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); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java new file mode 100644 index 000000000..f0d13d971 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java @@ -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 . */ + +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 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(); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooSupport.java new file mode 100644 index 000000000..5a718aea6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooSupport.java @@ -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 . */ + +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()); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0d712421..aa93dd155 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2432,4 +2432,5 @@ Send a reminder and enter sleep mode at bedtime. At the scheduled wake-up time, the wake-up alarm will sound. Xiaomi Watch S1 Active Mi Watch Color Sport + Pixoo