/* Copyright (C) 2020-2024 Andreas Shimokawa, Arjan Schrijver, Damien Gaignon, Daniel Thompson, Petr Vaněk 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.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;i8) 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 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(ArrayList weatherSpecs) { WeatherSpec weatherSpec = weatherSpecs.get(0); 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()); } } }