From 3c16b246a7c0a761ebf9c6543c4257f99e7f52f0 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 4 Dec 2019 16:42:28 +0000 Subject: [PATCH] Bangle.js: Tidy up data transmit and add data receive --- Banglejs.md | 26 ++- .../banglejs/BangleJSDeviceSupport.java | 190 +++++++++++++----- 2 files changed, 161 insertions(+), 55 deletions(-) diff --git a/Banglejs.md b/Banglejs.md index f5621a002..4d3965ab3 100644 --- a/Banglejs.md +++ b/Banglejs.md @@ -1,5 +1,15 @@ +https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Developer-Documentation + +```Bash +./gradlew assembleDebug adb install app/build/outputs/apk/debug/app-debug.apk +``` + +Messages sent to Bangle.js from Phone +-------------------------------------- + +wrapped in `GB(json)\n` `t:"notify", id:id, src,title,subject,body,sender,tel` - new notification `t:"notify-", id:id` - delete notification @@ -7,6 +17,18 @@ adb install app/build/outputs/apk/debug/app-debug.apk `t:"find", n:bool` - findDevice `t:"vibrate", n:int` - vibrate `t:"weather", temp,hum,txt,wind,loc` - weather report +`t:"musicstate", state,position,shuffle,repeat` +`t:"musicinfo", artist,album,track,dur,c(track count),n(track num)` -"musicstate", state,position,shuffle,repeat -"musicinfo", artist,album,track,dur,c(track count),n(track num) +Messages from Bangle.js to Phone +-------------------------------- + +Just raw newline-terminated JSON lines: + +`t:"info", msg:"..."` +`t:"warn", msg:"..."` +`t:"error", msg:"..."` +`t:"status", bat:0..100, volt:float(voltage)` - status update +`t:"findPhone", n:bool` +`t:"music", n:"play/pause/next/previous/volumeup/volumedown"` +`t:"call", n:"ACCEPT/END/INCOMING/OUTGOING/REJECT/START/IGNORE"` diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index 53c62a902..f2640616f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -1,9 +1,12 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.List; import java.util.TimeZone; import java.util.UUID; @@ -11,21 +14,33 @@ import java.util.UUID; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; import android.net.Uri; import android.widget.Toast; import androidx.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.json.JSONException; import org.json.JSONObject; import org.json.JSONArray; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; 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.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants; import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Constants; +import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySample; 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; @@ -40,7 +55,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(BangleJSDeviceSupport.class); - private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); public BluetoothGattCharacteristic rxCharacteristic = null; public BluetoothGattCharacteristic txCharacteristic = null; @@ -79,6 +93,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { /// Write a string of data, and chunk it up public void uartTx(TransactionBuilder builder, String str) { + LOG.info("UART TX: ", str); byte bytes[]; try { bytes = str.getBytes("UTF-8"); @@ -96,6 +111,97 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { } } + /// Write a string of data, and chunk it up + public 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); + } + } + + void handleUartRxLine(String line) { + LOG.info("UART RX LINE: " + line); + + if (line==">Uncaught ReferenceError: \"gb\" is not defined") + 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); + } + } + } + + 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": { + Context context = getContext(); + if (json.has("bat")) { + int b = json.getInt("bat"); + if (b<0) b=0; + if (b>100) b=100; + gbDevice.setBatteryLevel((short)b); + if (b < 30) { + gbDevice.setBatteryState(BatteryState.BATTERY_LOW); + GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(b)), "", context); + } else { + gbDevice.setBatteryState(BatteryState.BATTERY_NORMAL); + GB.removeBatteryNotification(context); + } + } + if (json.has("volt")) + gbDevice.setBatteryVoltage((float)json.getDouble("volt")); + gbDevice.sendDeviceUpdateIntent(context); + } 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 "activity": { + BangleJSActivitySample sample = new BangleJSActivitySample(); + 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(); + BangleJSSampleProvider provider = new BangleJSSampleProvider(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) { @@ -111,8 +217,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { int p = receivedLine.indexOf("\n"); String line = receivedLine.substring(0,p-1); receivedLine = receivedLine.substring(p+1); - LOG.info("RX LINE: " + line); - // TODO: parse this into JSON and handle it + handleUartRxLine(line); } } return false; @@ -120,19 +225,17 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { void setTime(TransactionBuilder builder) { - uartTx(builder, "\u0010setTime("+(System.currentTimeMillis()/1000)+");E.setTimeZone("+(TimeZone.getDefault().getRawOffset()/3600000)+");\n"); } @Override public boolean useAutoConnect() { - return false; + return true; } @Override public void onNotification(NotificationSpec notificationSpec) { try { - TransactionBuilder builder = performInitialized("onNotification"); JSONObject o = new JSONObject(); o.put("t", "notify"); o.put("id", notificationSpec.getId()); @@ -142,25 +245,21 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { o.put("body", notificationSpec.body); o.put("sender", notificationSpec.sender); o.put("tel", notificationSpec.phoneNumber); - - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error setting notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onNotification", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @Override public void onDeleteNotification(int id) { try { - TransactionBuilder builder = performInitialized("onDeleteNotification"); JSONObject o = new JSONObject(); o.put("t", "notify-"); o.put("id", id); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error deleting notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onDeleteNotification", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @@ -178,7 +277,6 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onSetAlarms(ArrayList alarms) { try { - TransactionBuilder builder = performInitialized("onSetAlarms"); JSONObject o = new JSONObject(); o.put("t", "alarm"); JSONArray jsonalarms = new JSONArray(); @@ -194,30 +292,26 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { jsonalarm.put("h", alarm.getHour()); jsonalarm.put("m", alarm.getMinute()); } - - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error setting alarms: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSetAlarms", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @Override public void onSetCallState(CallSpec callSpec) { try { - TransactionBuilder builder = performInitialized("onSetCallState"); 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); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error setting call state: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSetCallState", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } - } +} @Override public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { @@ -227,7 +321,6 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onSetMusicState(MusicStateSpec stateSpec) { try { - TransactionBuilder builder = performInitialized("onSetMusicState"); JSONObject o = new JSONObject(); o.put("t", "musicstate"); String musicStates[] = {"play","pause","stop",""}; @@ -235,17 +328,15 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { o.put("position", stateSpec.position); o.put("shuffle", stateSpec.shuffle); o.put("repeat", stateSpec.repeat); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error setting Music state: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSetMusicState", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @Override public void onSetMusicInfo(MusicSpec musicSpec) { try { - TransactionBuilder builder = performInitialized("onSetMusicInfo"); JSONObject o = new JSONObject(); o.put("t", "musicinfo"); o.put("artist", musicSpec.artist); @@ -254,10 +345,9 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { o.put("dur", musicSpec.duration); o.put("c", musicSpec.trackCount); o.put("n", musicSpec.trackNr); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error setting Music info: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSetMusicInfo", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @@ -319,28 +409,24 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onFindDevice(boolean start) { try { - TransactionBuilder builder = performInitialized("onFindDevice"); JSONObject o = new JSONObject(); o.put("t", "find"); o.put("n", start); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error finding device: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onFindDevice", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @Override public void onSetConstantVibration(int integer) { try { - TransactionBuilder builder = performInitialized("onSetConstantVibration"); JSONObject o = new JSONObject(); o.put("t", "vibrate"); o.put("n", integer); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error vibrating: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSetConstantVibration", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } @@ -387,7 +473,6 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onSendWeather(WeatherSpec weatherSpec) { try { - TransactionBuilder builder = performInitialized("onSendWeather"); JSONObject o = new JSONObject(); o.put("t", "weather"); o.put("temp", weatherSpec.currentTemp); @@ -395,10 +480,9 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { o.put("txt", weatherSpec.currentCondition); o.put("wind", weatherSpec.windSpeed); o.put("loc", weatherSpec.location); - uartTx(builder, "\u0010gb("+o.toString()+")\n"); - builder.queue(getQueue()); - } catch (Exception e) { - GB.toast(getContext(), "Error showing weather: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + uartTxJSON("onSendWeather", o); + } catch (JSONException e) { + LOG.info("JSONException: " + e.getLocalizedMessage()); } } }