package com.eveningoutpost.dexdrip.watch.lefun; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.os.PowerManager; import android.util.Pair; import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer; import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; import com.eveningoutpost.dexdrip.store.FastStore; import com.eveningoutpost.dexdrip.store.KeyStore; import com.eveningoutpost.dexdrip.utils.framework.IncomingCallsReceiver; import com.eveningoutpost.dexdrip.utils.framework.WakeLockTrampoline; import com.eveningoutpost.dexdrip.watch.PrefBindingFactory; import com.eveningoutpost.dexdrip.watch.lefun.messages.BaseRx; import com.eveningoutpost.dexdrip.watch.lefun.messages.BaseTx; import com.eveningoutpost.dexdrip.watch.lefun.messages.RxFind; import com.eveningoutpost.dexdrip.watch.lefun.messages.RxPong; import com.eveningoutpost.dexdrip.watch.lefun.messages.RxShake; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxAlert; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxPing; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxSetFeatures; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxSetLocaleFeature; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxSetScreens; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxSetTime; import com.eveningoutpost.dexdrip.watch.lefun.messages.TxShakeDetect; import com.eveningoutpost.dexdrip.xdrip; import com.polidea.rxandroidble2.RxBleDeviceServices; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.reactivex.schedulers.Schedulers; import static com.eveningoutpost.dexdrip.Models.ActiveBgAlert.currentlyAlerting; import static com.eveningoutpost.dexdrip.Models.JoH.bytesToHex; import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; import static com.eveningoutpost.dexdrip.Models.JoH.msTill; import static com.eveningoutpost.dexdrip.Models.JoH.niceTimeScalar; import static com.eveningoutpost.dexdrip.Models.JoH.roundDouble; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSE; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.DISCOVER; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.INIT; import static com.eveningoutpost.dexdrip.UtilityModels.Unitized.mmolConvert; import static com.eveningoutpost.dexdrip.watch.lefun.Const.REPLY_CHARACTERISTIC; import static com.eveningoutpost.dexdrip.watch.lefun.Const.WRITE_CHARACTERISTIC; import static com.eveningoutpost.dexdrip.watch.lefun.LeFun.shakeToSnooze; import static com.eveningoutpost.dexdrip.watch.lefun.LeFunService.LeFunState.ENABLE_NOTIFICATIONS; import static com.eveningoutpost.dexdrip.watch.lefun.LeFunService.LeFunState.PROTOTYPE; import static com.eveningoutpost.dexdrip.watch.lefun.LeFunService.LeFunState.QUEUE_MESSAGE; import static com.eveningoutpost.dexdrip.watch.lefun.LeFunService.LeFunState.SEND_SETTINGS; import static com.eveningoutpost.dexdrip.watch.lefun.LeFunService.LeFunState.SET_TIME; /** * Jamorham * * Data communication with Lefun compatible bands/watches */ public class LeFunService extends JamBaseBluetoothSequencer { private static final String MESSAGE = "LeFun-Message"; private static final String MESSAGE_TYPE = "LeFun-Message-Type"; private final KeyStore keyStore = FastStore.getInstance(); private static final boolean d = true; private static final long MAX_RETRY_BACKOFF_MS = Constants.SECOND_IN_MS * 300; // sleep for max ms if we have had no signal final Runnable canceller = () -> { if (!currentlyAlerting() && !IncomingCallsReceiver.isRingingNow()) { UserError.Log.d(TAG, "Clearing queue as alert / call ceased"); emptyQueue(); } }; { mState = new LeFunState().setLI(I); I.queue_write_characterstic = WRITE_CHARACTERISTIC; } @Override public int onStartCommand(Intent intent, int flags, int startId) { final PowerManager.WakeLock wl = JoH.getWakeLock("lefun service", 60000); try { if (shouldServiceRun()) { final String mac = LeFun.getMac(); if (emptyString(mac)) { // if mac not set then start up a scan and do nothing else new FindNearby().scan(); } else { setAddress(mac); if (intent != null) { final String function = intent.getStringExtra("function"); if (function != null) { switch (function) { case "refresh": changeState(INIT); break; case "prototype": changeState(PROTOTYPE); break; case "message": final String message = intent.getStringExtra("message"); final String message_type = intent.getStringExtra("message_type"); if (message != null) { keyStore.putS(MESSAGE, message); keyStore.putS(MESSAGE_TYPE, message_type != null ? message_type : ""); changeState(QUEUE_MESSAGE); } } } else { // no specific function UserError.Log.d(TAG, "SET TIME CALLED"); changeState(SET_TIME); } } } return START_STICKY; } else { UserError.Log.d(TAG, "Service is NOT set be active - shutting down"); stopSelf(); return START_NOT_STICKY; } } finally { JoH.releaseWakeLock(wl); } } private static final UUID[] huntCharacterstics = new UUID[]{REPLY_CHARACTERISTIC}; @Override protected void onServicesDiscovered(RxBleDeviceServices services) { boolean found = false; for (BluetoothGattService service : services.getBluetoothGattServices()) { UserError.Log.d(TAG, "Service: " + getUUIDName(service.getUuid())); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { UserError.Log.d(TAG, "-- Character: " + getUUIDName(characteristic.getUuid())); for (final UUID check : huntCharacterstics) { if (characteristic.getUuid().equals(check)) { I.readCharacteristic = check; found = true; } } } } if (found) { I.isDiscoveryComplete = true; enableNotification(); } else { UserError.Log.e(TAG, "Could not find characteristic during service discovery. This is very unusual"); } } private void enableNotification() { UserError.Log.d(TAG, "Enabling notifications"); I.isNotificationEnabled = false; if (I.connection == null) { UserError.Log.d(TAG, "Cannot enable as connection is null!"); return; } if (I.readCharacteristic == null) { UserError.Log.d(TAG, "Cannot enable as read characterstic is null"); return; } I.connection.setupNotification(I.readCharacteristic) .timeout(630, TimeUnit.SECONDS) // WARN //.observeOn(Schedulers.newThread()) // needed? .doOnNext(notificationObservable -> { I.isNotificationEnabled = true; // change to queue send state changeState(mState.next()); }).flatMap(notificationObservable -> notificationObservable) //.timeout(5, TimeUnit.SECONDS) .observeOn(Schedulers.newThread()) .subscribe(bytes -> { // incoming notifications if (d) UserError.Log.d(TAG, "Received data notification bytes: " + HexDump.dumpHexString(bytes)); processAndAction(bytes); }, throwable -> { if (!(throwable instanceof TimeoutException)) { UserError.Log.e(TAG, "Throwable inside setup notification: " + throwable); } else { UserError.Log.d(TAG, "OUTER TIMEOUT INSIDE NOTIFICATION LISTENER"); } I.isNotificationEnabled = false; changeState(CLOSE); // stopConnect(); } ); } private void processAndAction(final byte[] bytes) { final String incomingHex = bytesToHex(bytes); switch (incomingHex) { // non classified literal values can go here default: final BaseRx packet = Classifier.classify(bytes); if (packet != null) { UserError.Log.d(TAG, "Classified: " + packet.getClass().getSimpleName()); if (packet instanceof RxPong) { LeFun.setModel(((RxPong) packet).getModel()); } else if (packet instanceof RxShake) { shakeDetected(); } else if (packet instanceof RxFind) { findPhone(); } } } } private void shakeDetected() { UserError.Log.d(TAG, "Shake detected"); if (shakeToSnooze()) { AlertPlayer.getPlayer().OpportunisticSnooze(); emptyQueue(); UserError.Log.ueh(TAG, "Alert snoozed by Shake"); } } private void findPhone() { UserError.Log.d(TAG, "Find phone function triggered"); if (!AlertPlayer.getPlayer().OpportunisticSnooze()) { JoH.showNotification("Find Phone", "Activated from Lefun band", null, 5, true, true, false); } else { emptyQueue(); UserError.Log.ueh(TAG, "Alert snoozed by Find feature"); } } private void sendSettings() { probeModelTypeIfUnknown(); for (Pair<Integer, Boolean> lState : PrefBindingFactory.getInstance(LefunPrefBinding.class).getStates("lefun_locale_")) { new QueueMe() .setBytes(new TxSetLocaleFeature(lState.first, lState.second).getBytes()) .setDescription("Set Locale Features") .expectReply().expireInSeconds(30) .queue(); } BaseTx screens = new TxSetScreens(); for (int screen : PrefBindingFactory.getInstance(LefunPrefBinding.class).getEnabled("lefun_screen")) { screens.enable(screen); } new QueueMe() .setBytes(screens.getBytes()) .setDescription("Set screens for: ") .expectReply().expireInSeconds(30) .queue(); BaseTx features = new TxSetFeatures(); for (int feature : PrefBindingFactory.getInstance(LefunPrefBinding.class).getEnabled("lefun_feature")) { features.enable(feature); } new QueueMe() .setBytes(features.getBytes()) .setDescription("Set features for: ") .expectReply().expireInSeconds(30) .send(); } private void prototype() { LeFun.sendAlert("TEST", "12.3"); /* new QueueMe() .setBytes(new TxSetLang().getBytes()) .setDescription("Set prototype lang") .expectReply().expireInSeconds(30) .queue(); */ startQueueSend(); } private void probeModelTypeIfUnknown() { if (emptyString(LeFun.getModel())) { new QueueMe() .setBytes(new TxPing().getBytes()) .setDescription("Set Probe model type") .expectReply().expireInSeconds(30) .queue(); } } private void sendBG() { // TODO use DisplayGlucose 100% and avoid rounding errors final BgReading last = BgReading.last(); FunAlmanac.Reply rep; if (last == null || last.isStale()) { rep = FunAlmanac.getRepresentation(0); } else { final double mmol_value = roundDouble(mmolConvert(last.getDg_mgdl()), 1); rep = FunAlmanac.getRepresentation(mmol_value); } UserError.Log.uel(TAG, "Representation for: " + rep.input); probeModelTypeIfUnknown(); new QueueMe() .setBytes(new TxSetTime(rep.timestamp, rep.zeroMonth, rep.zeroDay).getBytes()) .setDescription("Set display for: " + rep.input) .expectReply().expireInSeconds(290) .send(); } private void queueMessage() { final String alert = keyStore.getS(MESSAGE); final String type = keyStore.getS(MESSAGE_TYPE); UserError.Log.d(TAG,"Queuing message alert of type: "+type+" "+alert); if (!emptyString(alert)) { probeModelTypeIfUnknown(); switch (type != null ? type : "null") { case "call": for (int repeats = 0; repeats < 25; repeats++) { new QueueMe() .setBytes(new TxAlert(alert, TxAlert.ICON_CALL).getBytes()) .setDescription("Send call alert: " + alert) .expectReply().expireInSeconds(60) .setDelayMs(5000) .setRunnable(canceller) .queue(); } UserError.Log.d(TAG, "Queued call alert: " + alert); break; default: // glucose for (int repeats = 0; repeats < 5; repeats++) { new QueueMe() .setBytes(new TxShakeDetect(false).getBytes()) .setDescription("Disable Shake detection") .expectReply().expireInSeconds(60) .setRunnable(canceller) .queue(); new QueueMe() .setBytes(new TxAlert(alert).getBytes()) .setDescription("Send alert: " + alert) .expectReply().expireInSeconds(60) .setDelayMs(shakeToSnooze() ? 1500 : 200) .queue(); if (shakeToSnooze()) { new QueueMe() .setBytes(new TxShakeDetect(true).getBytes()) .setDescription("Enable Shake detection") .expectReply().expireInSeconds(60) .setDelayMs(10000) .queue(); } } break; } // this parent method might get called multiple times Inevitable.task("lefun-s-queue", 200, () -> changeState(mState.next())); } else { UserError.Log.e(TAG, "Alert message requested but no message set"); } } static class LeFunState extends JamBaseBluetoothSequencer.BaseState { static final String SET_TIME = "Setting Time"; static final String SEND_SETTINGS = "Updating Settings"; static final String QUEUE_MESSAGE = "Sending Alert"; static final String PROTOTYPE = "Prototype Test"; static final String ENABLE_NOTIFICATIONS = "Enabling notify"; { sequence.clear(); sequence.add(INIT); sequence.add(CONNECT_NOW); sequence.add(SEND_QUEUE); sequence.add(SEND_SETTINGS); sequence.add(SET_TIME); sequence.add(SLEEP); // sequence.add(QUEUE_MESSAGE); sequence.add(SEND_QUEUE); sequence.add(SLEEP); // sequence.add(PROTOTYPE); sequence.add(SEND_QUEUE); sequence.add(SLEEP); // sequence.add(ENABLE_NOTIFICATIONS); sequence.add(SEND_QUEUE); sequence.add(SLEEP); } } @Override protected synchronized boolean automata() { extendWakeLock(1000); UserError.Log.d(TAG, "Automata called in LeFun"); if (I.state.equals(QUEUE_MESSAGE) || alwaysConnected()) { if ((I.isConnected) && !I.state.equals(CLOSE)) { if (!I.isDiscoveryComplete) { UserError.Log.d(TAG, "Services not discovered"); I.state = DISCOVER; } else if ((!I.isNotificationEnabled) && (JoH.ratelimit("lefun-enable-notifications", 2))) { UserError.Log.d(TAG, "Notifications not enabled"); I.state = ENABLE_NOTIFICATIONS; } } switch (I.state) { case INIT: // connect by default changeState(mState.next()); break; case ENABLE_NOTIFICATIONS: enableNotification(); break; case SEND_SETTINGS: sendSettings(); break; case SET_TIME: sendBG(); break; case PROTOTYPE: prototype(); break; case QUEUE_MESSAGE: queueMessage(); break; default: return super.automata(); } } return true; // lies } private boolean shouldServiceRun() { return LeFunEntry.isEnabled(); } @Override protected void setRetryTimerReal() { if (shouldServiceRun()) { final long retry_in = whenToRetryNext(); UserError.Log.d(TAG, "setRetryTimer: Restarting in: " + (retry_in / Constants.SECOND_IN_MS) + " seconds"); I.serviceIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.LEFUN_SERVICE_RETRY_ID); //PendingIntent.getService(xdrip.getAppContext(), Constants.LEFUN_SERVICE_RETRY_ID, // new Intent(xdrip.getAppContext(), this.getClass()), 0); I.retry_time = JoH.wakeUpIntent(xdrip.getAppContext(), retry_in, I.serviceIntent); I.wakeup_time = JoH.tsl() + retry_in; } else { UserError.Log.d(TAG, "Not setting retry timer as service should not be running"); } } private long whenToRetryNext() { I.retry_backoff += Constants.SECOND_IN_MS; if (I.retry_backoff > MAX_RETRY_BACKOFF_MS) { I.retry_backoff = MAX_RETRY_BACKOFF_MS; } return Constants.SECOND_IN_MS * 10 + I.retry_backoff; } // Mega Status public static List<StatusItem> megaStatus() { final List<StatusItem> l = new ArrayList<>(); final Inst II = Inst.get(LeFunService.class.getSimpleName()); l.add(new StatusItem("Model", LeFun.getModel())); l.add(new StatusItem("Mac address", LeFun.getMac())); l.add(new StatusItem("Connected", II.isConnected ? "Yes" : "No")); if (II.wakeup_time != 0) { final long till = msTill(II.wakeup_time); if (till > 0) l.add(new StatusItem("Wake Up", niceTimeScalar(till))); } // if (II.retry_time != 0) { // l.add(new StatusItem("Retry", niceTimeScalar(msTill(II.retry_time)))); // } l.add(new StatusItem("State", II.state)); final int qsize = II.getQueueSize(); if (qsize > 0) { l.add(new StatusItem("Queue", qsize + " items")); } return l; } }