mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-07-01 17:26:18 +02:00
401 lines
16 KiB
Java
401 lines
16 KiB
Java
/* Copyright (C) 2019-2020 Andreas Shimokawa, Gordon Williams
|
|
|
|
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.waspos;
|
|
|
|
import android.bluetooth.BluetoothGatt;
|
|
import android.bluetooth.BluetoothGattCharacteristic;
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
import android.text.format.DateFormat;
|
|
import android.widget.Toast;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Calendar;
|
|
import java.util.UUID;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSConstants;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
|
|
|
public class WaspOSDeviceSupport extends AbstractBTLEDeviceSupport {
|
|
private static final Logger LOG = LoggerFactory.getLogger(WaspOSDeviceSupport.class);
|
|
private BluetoothGattCharacteristic rxCharacteristic = null;
|
|
private BluetoothGattCharacteristic txCharacteristic = null;
|
|
|
|
private String receivedLine = "";
|
|
|
|
public WaspOSDeviceSupport() {
|
|
super(LOG);
|
|
addSupportedService(WaspOSConstants.UUID_SERVICE_NORDIC_UART);
|
|
}
|
|
|
|
@Override
|
|
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
|
LOG.info("Initializing");
|
|
|
|
gbDevice.setState(GBDevice.State.INITIALIZING);
|
|
gbDevice.sendDeviceUpdateIntent(getContext());
|
|
|
|
rxCharacteristic = getCharacteristic(WaspOSConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX);
|
|
txCharacteristic = getCharacteristic(WaspOSConstants.UUID_CHARACTERISTIC_NORDIC_UART_TX);
|
|
builder.setCallback(this);
|
|
builder.notify(rxCharacteristic, true);
|
|
|
|
uartTx(builder, " \u0003"); // clear active line
|
|
|
|
Prefs prefs = GBApplication.getPrefs();
|
|
if (prefs.getBoolean("datetime_synconconnect", true))
|
|
setTime(builder);
|
|
//sendSettings(builder);
|
|
|
|
// get version
|
|
|
|
gbDevice.setState(GBDevice.State.INITIALIZED);
|
|
gbDevice.sendDeviceUpdateIntent(getContext());
|
|
|
|
LOG.info("Initialization Done");
|
|
|
|
return builder;
|
|
}
|
|
|
|
/// Write a string of data, and chunk it up
|
|
private void uartTx(TransactionBuilder builder, String str) {
|
|
LOG.info("UART TX: " + str);
|
|
byte[] bytes;
|
|
bytes = str.getBytes(StandardCharsets.ISO_8859_1);
|
|
for (int i=0;i<bytes.length;i+=8) {
|
|
int l = bytes.length-i;
|
|
if (l>8) l=8;
|
|
byte[] packet = new byte[l];
|
|
System.arraycopy(bytes, i, packet, 0, l);
|
|
builder.write(txCharacteristic, packet);
|
|
}
|
|
}
|
|
|
|
/// Write a string of data, and chunk it up
|
|
private void uartTxJSON(String taskName, JSONObject json) {
|
|
try {
|
|
TransactionBuilder builder = performInitialized(taskName);
|
|
uartTx(builder, "\u0010GB("+json.toString()+")\n");
|
|
builder.queue(getQueue());
|
|
} catch (IOException e) {
|
|
GB.toast(getContext(), "Error in "+taskName+": " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
|
}
|
|
}
|
|
|
|
private void handleUartRxLine(String line) {
|
|
LOG.info("UART RX LINE: " + line);
|
|
|
|
if (">Uncaught ReferenceError: \"gb\" is not defined".equals(line))
|
|
GB.toast(getContext(), "Gadgetbridge plugin not installed on Bangle.js", Toast.LENGTH_LONG, GB.ERROR);
|
|
else if (line.charAt(0)=='{') {
|
|
// JSON - we hope!
|
|
try {
|
|
JSONObject json = new JSONObject(line);
|
|
handleUartRxJSON(json);
|
|
} catch (JSONException e) {
|
|
GB.toast(getContext(), "Malformed JSON from Bangle.js: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void handleUartRxJSON(JSONObject json) throws JSONException {
|
|
switch (json.getString("t")) {
|
|
case "info":
|
|
GB.toast(getContext(), "Bangle.js: " + json.getString("msg"), Toast.LENGTH_LONG, GB.INFO);
|
|
break;
|
|
case "warn":
|
|
GB.toast(getContext(), "Bangle.js: " + json.getString("msg"), Toast.LENGTH_LONG, GB.WARN);
|
|
break;
|
|
case "error":
|
|
GB.toast(getContext(), "Bangle.js: " + json.getString("msg"), Toast.LENGTH_LONG, GB.ERROR);
|
|
break;
|
|
case "status": {
|
|
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
|
if (json.has("bat")) {
|
|
int b = json.getInt("bat");
|
|
if (b < 0) b = 0;
|
|
if (b > 100) b = 100;
|
|
batteryInfo.level = b;
|
|
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
|
}
|
|
if (json.has("volt"))
|
|
batteryInfo.voltage = (float) json.getDouble("volt");
|
|
handleGBDeviceEvent(batteryInfo);
|
|
} break;
|
|
case "findPhone": {
|
|
boolean start = json.has("n") && json.getBoolean("n");
|
|
GBDeviceEventFindPhone deviceEventFindPhone = new GBDeviceEventFindPhone();
|
|
deviceEventFindPhone.event = start ? GBDeviceEventFindPhone.Event.START : GBDeviceEventFindPhone.Event.STOP;
|
|
evaluateGBDeviceEvent(deviceEventFindPhone);
|
|
} break;
|
|
case "music": {
|
|
GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
|
|
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.valueOf(json.getString("n").toUpperCase());
|
|
evaluateGBDeviceEvent(deviceEventMusicControl);
|
|
} break;
|
|
case "call": {
|
|
GBDeviceEventCallControl deviceEventCallControl = new GBDeviceEventCallControl();
|
|
deviceEventCallControl.event = GBDeviceEventCallControl.Event.valueOf(json.getString("n").toUpperCase());
|
|
evaluateGBDeviceEvent(deviceEventCallControl);
|
|
} break;
|
|
case "notify" : {
|
|
GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl();
|
|
// .title appears unused
|
|
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.valueOf(json.getString("n").toUpperCase());
|
|
if (json.has("id"))
|
|
deviceEvtNotificationControl.handle = json.getInt("id");
|
|
if (json.has("tel"))
|
|
deviceEvtNotificationControl.phoneNumber = json.getString("tel");
|
|
if (json.has("msg"))
|
|
deviceEvtNotificationControl.reply = json.getString("msg");
|
|
evaluateGBDeviceEvent(deviceEvtNotificationControl);
|
|
} break;
|
|
/*case "activity": {
|
|
WaspOSActivitySample sample = new WaspOSActivitySample();
|
|
sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
|
|
sample.setHeartRate(json.getInteger("hrm"));
|
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
|
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
|
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
|
|
WaspOSSampleProvider provider = new WaspOSSampleProvider(getDevice(), dbHandler.getDaoSession());
|
|
sample.setDeviceId(deviceId);
|
|
sample.setUserId(userId);
|
|
provider.addGBActivitySample(sample);
|
|
} catch (Exception ex) {
|
|
LOG.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
|
|
}
|
|
} break;*/
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
|
BluetoothGattCharacteristic characteristic) {
|
|
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
|
return true;
|
|
}
|
|
if (WaspOSConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX.equals(characteristic.getUuid())) {
|
|
byte[] chars = characteristic.getValue();
|
|
String packetStr = new String(chars);
|
|
LOG.info("RX: " + packetStr);
|
|
receivedLine += packetStr;
|
|
while (receivedLine.contains("\n")) {
|
|
int p = receivedLine.indexOf("\n");
|
|
String line = receivedLine.substring(0,p-1);
|
|
receivedLine = receivedLine.substring(p+1);
|
|
handleUartRxLine(line);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void setTime(TransactionBuilder builder) {
|
|
CharSequence formattedDate = DateFormat.format("(yyyy, MM, dd, HH, mm, ss)", new java.util.Date());
|
|
String cmd = "\u0010watch.rtc.set_localtime(" + formattedDate + ")\n";
|
|
uartTx(builder, cmd + "\n");
|
|
}
|
|
|
|
@Override
|
|
public boolean useAutoConnect() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onNotification(NotificationSpec notificationSpec) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "notify");
|
|
o.put("id", notificationSpec.getId());
|
|
o.put("src", notificationSpec.sourceName);
|
|
o.put("title", notificationSpec.title);
|
|
o.put("subject", notificationSpec.subject);
|
|
o.put("body", notificationSpec.body);
|
|
o.put("sender", notificationSpec.sender);
|
|
o.put("tel", notificationSpec.phoneNumber);
|
|
uartTxJSON("onNotification", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDeleteNotification(int id) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "notify-");
|
|
o.put("id", id);
|
|
uartTxJSON("onDeleteNotification", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetTime() {
|
|
try {
|
|
TransactionBuilder builder = performInitialized("setTime");
|
|
setTime(builder);
|
|
builder.queue(getQueue());
|
|
} catch (Exception e) {
|
|
GB.toast(getContext(), "Error setting time: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "alarm");
|
|
JSONArray jsonalarms = new JSONArray();
|
|
o.put("d", jsonalarms);
|
|
|
|
for (Alarm alarm : alarms) {
|
|
if (!alarm.getEnabled()) continue;
|
|
JSONObject jsonalarm = new JSONObject();
|
|
jsonalarms.put(jsonalarm);
|
|
|
|
Calendar calendar = AlarmUtils.toCalendar(alarm);
|
|
// TODO: getRepetition to ensure it only happens on correct day?
|
|
jsonalarm.put("h", alarm.getHour());
|
|
jsonalarm.put("m", alarm.getMinute());
|
|
}
|
|
uartTxJSON("onSetAlarms", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetCallState(CallSpec callSpec) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "call");
|
|
String[] cmdString = {"", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end"};
|
|
o.put("cmd", cmdString[callSpec.command]);
|
|
o.put("name", callSpec.name);
|
|
o.put("number", callSpec.number);
|
|
uartTxJSON("onSetCallState", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetMusicState(MusicStateSpec stateSpec) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "musicstate");
|
|
String[] musicStates = {"play", "pause", "stop", ""};
|
|
o.put("state", musicStates[stateSpec.state]);
|
|
o.put("position", stateSpec.position);
|
|
o.put("shuffle", stateSpec.shuffle);
|
|
o.put("repeat", stateSpec.repeat);
|
|
uartTxJSON("onSetMusicState", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetMusicInfo(MusicSpec musicSpec) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "musicinfo");
|
|
o.put("artist", musicSpec.artist);
|
|
o.put("album", musicSpec.album);
|
|
o.put("track", musicSpec.track);
|
|
o.put("dur", musicSpec.duration);
|
|
o.put("c", musicSpec.trackCount);
|
|
o.put("n", musicSpec.trackNr);
|
|
uartTxJSON("onSetMusicInfo", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFindDevice(boolean start) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "find");
|
|
o.put("n", start);
|
|
uartTxJSON("onFindDevice", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSetConstantVibration(int integer) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "vibrate");
|
|
o.put("n", integer);
|
|
uartTxJSON("onSetConstantVibration", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSendWeather(WeatherSpec weatherSpec) {
|
|
try {
|
|
JSONObject o = new JSONObject();
|
|
o.put("t", "weather");
|
|
o.put("temp", weatherSpec.currentTemp);
|
|
o.put("hum", weatherSpec.currentHumidity);
|
|
o.put("code", weatherSpec.currentConditionCode);
|
|
o.put("txt", weatherSpec.currentCondition);
|
|
o.put("wind", weatherSpec.windSpeed);
|
|
o.put("loc", weatherSpec.location);
|
|
uartTxJSON("onSendWeather", o);
|
|
} catch (JSONException e) {
|
|
LOG.info("JSONException: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
}
|