From 328009256687ead752ae04416c30873d6081a460 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Thu, 13 Jul 2017 19:50:11 +0200 Subject: [PATCH] Source ruffy scripter for the time being (already some fixes and tweaks in). --- .../d/ruffy/ruffy/driver/IRTHandler.aidl | 21 ++ .../d/ruffy/ruffy/driver/IRuffyService.aidl | 14 + .../d/ruffy/ruffy/driver/display/Menu.aidl | 3 + .../jotomo/ruffyscripter/RuffyScripter.java | 286 ++++++++++++++++++ .../ruffyscripter/commands/BolusCommand.java | 89 ++++++ .../commands/CancelTbrCommand.java | 20 ++ .../ruffyscripter/commands/Command.java | 11 + .../commands/CommandException.java | 44 +++ .../ruffyscripter/commands/CommandResult.java | 41 +++ .../ruffyscripter/commands/SetTbrCommand.java | 238 +++++++++++++++ .../d/ruffy/ruffy/driver/display/Menu.java | 123 ++++++++ .../ruffy/driver/display/MenuAttribute.java | 34 +++ .../ruffy/ruffy/driver/display/MenuType.java | 42 +++ .../ruffy/driver/display/menu/BolusType.java | 13 + .../ruffy/driver/display/menu/MenuBlink.java | 12 + .../ruffy/driver/display/menu/MenuDate.java | 27 ++ .../ruffy/driver/display/menu/MenuTime.java | 36 +++ 17 files changed, 1054 insertions(+) create mode 100644 app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl create mode 100644 app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl create mode 100644 app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/BolusCommand.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/CancelTbrCommand.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/Command.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/CommandException.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/CommandResult.java create mode 100644 app/src/main/java/de/jotomo/ruffyscripter/commands/SetTbrCommand.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java create mode 100644 app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl new file mode 100644 index 0000000000..996b10b666 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl @@ -0,0 +1,21 @@ +// IRTHandler.aidl +package org.monkey.d.ruffy.ruffy.driver; + +// Declare any non-default types here with import statements + +import org.monkey.d.ruffy.ruffy.driver.display.Menu; + +interface IRTHandler { + void log(String message); + void fail(String message); + + void requestBluetooth(); + void rtStopped(); + void rtStarted(); + + void rtClearDisplay(); + void rtUpdateDisplay(in byte[] quarter, int which); + + void rtDisplayHandleMenu(in Menu menu); + void rtDisplayHandleNoMenu(); +} diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl new file mode 100644 index 0000000000..3baa4116e1 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl @@ -0,0 +1,14 @@ +// IRuffyService.aidl +package org.monkey.d.ruffy.ruffy.driver; + +// Declare any non-default types here with import statements +import org.monkey.d.ruffy.ruffy.driver.IRTHandler; + +interface IRuffyService { + + void setHandler(IRTHandler handler); + int doRTConnect(); + void doRTDisconnect(); + void rtSendKey(byte keyCode, boolean changed); + void resetPairing(); +} diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl new file mode 100644 index 0000000000..f0b99ec918 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl @@ -0,0 +1,3 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +parcelable Menu; \ No newline at end of file diff --git a/app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java b/app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java new file mode 100644 index 0000000000..3c25f325cf --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java @@ -0,0 +1,286 @@ +package de.jotomo.ruffyscripter; + +import android.os.RemoteException; +import android.os.SystemClock; + +import org.monkey.d.ruffy.ruffy.driver.IRTHandler; +import org.monkey.d.ruffy.ruffy.driver.IRuffyService; +import org.monkey.d.ruffy.ruffy.driver.display.Menu; +import org.monkey.d.ruffy.ruffy.driver.display.MenuType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jotomo.ruffyscripter.commands.Command; +import de.jotomo.ruffyscripter.commands.CommandException; +import de.jotomo.ruffyscripter.commands.CommandResult; + +// TODO regularly read "My data" history (boluses, TBR) to double check all commands ran successfully. +// Automatically compare against AAPS db, or log all requests in the PumpInterface (maybe Milos +// already logs those requests somewhere ... and verify they have all been ack'd by the pump properly + +/** + * provides scripting 'runtime' and operations. consider moving operations into a separate + * class and inject that into executing commands, so that commands operately solely on + * operations and are cleanly separated from the thread management, connection management etc + */ +public class RuffyScripter { + private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class); + + public volatile Menu currentMenu; + + private final IRuffyService ruffyService; + private volatile CommandResult cmdResult; + private volatile long menuLastUpdated = 0; + + public RuffyScripter(IRuffyService ruffyService) { + this.ruffyService = ruffyService; + try { + ruffyService.setHandler(mHandler); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + private IRTHandler mHandler = new IRTHandler.Stub() { + @Override + public void log(String message) throws RemoteException { + log.trace(message); + } + + @Override + public void fail(String message) throws RemoteException { + log.warn(message); + } + + @Override + public void requestBluetooth() throws RemoteException { + log.trace("Ruffy invoked requestBluetooth callback"); + } + + @Override + public void rtStopped() throws RemoteException { + log.debug("rtStopped callback invoked"); + currentMenu = null; + } + + @Override + public void rtStarted() throws RemoteException { + log.debug("rtStarted callback invoked"); + } + + @Override + public void rtClearDisplay() throws RemoteException { + } + + @Override + public void rtUpdateDisplay(byte[] quarter, int which) throws RemoteException { + } + + @Override + public void rtDisplayHandleMenu(Menu menu) throws RemoteException { + // method is called every ~500ms + log.debug("rtDisplayHandleMenu: " + menu.getType()); + + currentMenu = menu; + menuLastUpdated = System.currentTimeMillis(); + + // note that a WARNING_OR_ERROR menu can be a valid temporary state (cancelling TBR) + // of a running command + if (activeCmd == null && currentMenu.getType() == MenuType.WARNING_OR_ERROR) { + log.warn("Warning/error menu encountered without a command running"); + } + } + + @Override + public void rtDisplayHandleNoMenu() throws RemoteException { + log.debug("rtDisplayHandleNoMenu callback invoked"); + } + + }; + + public boolean isPumpBusy() { + return activeCmd != null; // || currentMenu == null || currentMenu.getType() != MenuType.MAIN_MENU; + } + + private volatile Command activeCmd = null; + + // TODO take a callback to call when command finishes? + // TODO sometimes hangs ... we should timeout and raise an alert to check the pump and ruffy + // TODO not getting a menu update in a while is a good cue ... + // fire up a monitoring thread for that? + public CommandResult runCommand(final Command cmd) { + try { + if (isPumpBusy()) { + return new CommandResult().message("Pump is busy"); + } + ensureConnected(); + + // TODO make this a safe lock + synchronized (this) { + cmdResult = null; + activeCmd = cmd; + final RuffyScripter scripter = this; + // TODO hackish, to say the least ... + // wait till pump is ready for input + waitForMenuUpdate(); + log.debug("Cmd execution: connection ready, executing cmd " + cmd); + new Thread(new Runnable() { + @Override + public void run() { + try { + cmdResult = cmd.execute(scripter); + } catch (Exception e) { + cmdResult = new CommandResult().exception(e).message("Unexpected exception running cmd"); + } finally { + activeCmd = null; + } + + } + }).start(); + } + + // TODO really? + while (activeCmd != null) { + SystemClock.sleep(500); + log.trace("Waiting for running command to complete"); + } + log.debug("Command result: " + cmdResult); + + CommandResult r = cmdResult; + cmdResult = null; + return r; + } catch (CommandException e) { + return e.toCommandResult(); + } catch (Exception e) { + return new CommandResult().exception(e).message("Unexpected exception communication with ruffy"); + } + } + + public void ensureConnected() { + // did we get a menu update from the pump in the last 5s? Then we're connected + if (currentMenu != null && menuLastUpdated + 5000 > System.currentTimeMillis()) { + log.debug("Pump is sending us menu updating, so we're connected"); + return; + } + + try { + boolean connectSuccesful = ruffyService.doRTConnect() == 0; + log.debug("Connect init successful: " + connectSuccesful); + while (currentMenu == null) { + log.debug("Waiting for first menu update to be sent"); + waitForMenuUpdate(); + } + } catch (RemoteException e) { + throw new CommandException().exception(e).message("Unexpected exception while initiating/restoring pump connection"); + } + } + + public CommandResult disconnect() { + try { + ruffyService.doRTDisconnect(); + } catch (RemoteException e) { + return new CommandResult().exception(e).message("Unexpected exception trying to disconnect"); + } + return new CommandResult().success(true); + } + + // below: methods to be used by commands + + private static class Key { + static byte NO_KEY = (byte) 0x00; + static byte MENU = (byte) 0x03; + static byte CHECK = (byte) 0x0C; + static byte UP = (byte) 0x30; + static byte DOWN = (byte) 0xC0; + } + + public void pressUpKey() { + pressKey(Key.UP); + } + + public void pressDownKey() { + pressKey(Key.DOWN); + } + + public void pressCheckKey() { + pressKey(Key.CHECK); + } + + public void pressMenuKey() { + // TODO build 'wait for menu update' into this method? get current menu, press key, wait for update? + pressKey(Key.MENU); + } + + /** Wait until the menu update is in */ + public void waitForMenuUpdate() { + long timeoutExpired = System.currentTimeMillis() + 90 * 1000; + long initialUpdateTime = menuLastUpdated; + while (initialUpdateTime == menuLastUpdated) { + if(System.currentTimeMillis() > timeoutExpired) { + throw new CommandException().message("Timeout waiting for menu update"); + } + SystemClock.sleep(50); + } + } + + /** + * "Virtual" key, emulated by pressing menu and up simultaneously + */ + // Doesn't work +/* public void pressBackKey() throws RemoteException { + ruffyService.rtSendKey(Key.MENU, true); + SystemClock.sleep(50); + ruffyService.rtSendKey(Key.UP, true); + SystemClock.sleep(100); + ruffyService.rtSendKey(Key.NO_KEY, true); + }*/ + + private void pressKey(final byte key) { + try { + ruffyService.rtSendKey(key, true); + SystemClock.sleep(100); + ruffyService.rtSendKey(Key.NO_KEY, true); + } catch (RemoteException e) { + throw new CommandException().exception(e).message("Error while pressing buttons"); + } + } + + public void navigateToMenu(MenuType desiredMenu) { + // TODO menu var might not have been initialized if this is called to early + // though that's gonna be a problem for all code; + // wait during init till this is set? create a getter for currentMenu to do this? + MenuType startedFrom = currentMenu.getType(); + while (currentMenu.getType() != desiredMenu) { + MenuType currentType = currentMenu.getType(); +/* if (currentType == startedFrom) { + // TODO don't trigger right away, that's always a match ;-) + // failed to find the menu, after going through all the menus, bail out + throw new CommandException(false, null, "Menu not found searching for " + desiredMenu); + }*/ + pressMenuKey(); + waitForMenuToBeLeft(currentType); + } + } + + /** + * Wait till a menu changed has completed, "away" from the menu provided as argument. + */ + public void waitForMenuToBeLeft(MenuType menuType) { + while (currentMenu.getType() == menuType) { + SystemClock.sleep(250); + } + } + + public void verifyMenuIsDisplayed(MenuType menu) { + String message = "Invalid pump state, expected to be in menu " + + menu + ", but current menu is " + currentMenu.getType(); + verifyMenuIsDisplayed(menu, message); + } + + public void verifyMenuIsDisplayed(MenuType menu, String message) { + waitForMenuUpdate(); + if (currentMenu.getType() != menu) { + throw new CommandException().message(message); + } + } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/BolusCommand.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/BolusCommand.java new file mode 100644 index 0000000000..2550c121e5 --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/BolusCommand.java @@ -0,0 +1,89 @@ +package de.jotomo.ruffyscripter.commands; + +import android.os.SystemClock; + +import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute; +import org.monkey.d.ruffy.ruffy.driver.display.MenuType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +import de.jotomo.ruffyscripter.RuffyScripter; + +public class BolusCommand implements Command { + private static final Logger log = LoggerFactory.getLogger(BolusCommand.class); + + private final double bolus; + + public BolusCommand(double bolus) { + this.bolus = bolus; + } + + @Override + public CommandResult execute(RuffyScripter scripter) { + try { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + enterBolusMenu(scripter); + inputBolusAmount(scripter); + SystemClock.sleep(500); + verifyDisplayedBolusAmount(scripter); + + // confirm bolus + scripter.pressCheckKey(); + + // the pump displays the entered bolus and waits a bit to let user check and cancel + scripter.waitForMenuToBeLeft(MenuType.BOLUS_ENTER); + + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU, + "Pump did not return to MAIN_MEU from BOLUS_ENTER to deliver bolus. " + + "Check pump manually, the bolus might not have been delivered."); + + // wait for bolus delivery to complete + Double bolusRemaining = (Double) scripter.currentMenu.getAttribute(MenuAttribute.BOLUS_REMAINING); + while (bolusRemaining != null) { + log.debug("Delivering bolus, remaining: " + bolusRemaining); + SystemClock.sleep(200); + bolusRemaining = (Double) scripter.currentMenu.getAttribute(MenuAttribute.BOLUS_REMAINING); + } + + // TODO what if we hit 'cartridge low' alert here? is it immediately displayed or after the bolus? + // TODO how are error states reported back to the caller that occur outside of calls in genal? Low battery, low cartridge? + + // make sure no alert (occlusion, cartridge empty) has occurred. + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU, + "Bolus delivery did not complete as expected. " + + "Check pump manually, the bolus might not have been delivered."); + + return new CommandResult().success(true).enacted(true) + .message(String.format(Locale.US, "Delivered %02.1f U", bolus)); + } catch (CommandException e) { + return e.toCommandResult(); + } + } + + private void enterBolusMenu(RuffyScripter scripter) { + scripter.navigateToMenu(MenuType.BOLUS_MENU); + scripter.pressCheckKey(); + scripter.waitForMenuUpdate(); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + } + + private void inputBolusAmount(RuffyScripter scripter) { + // press 'up' once for each 0.1 U increment + long steps = Math.round(bolus * 10); + for (int i = 0; i < steps; i++) { + scripter.pressUpKey(); + SystemClock.sleep(100); + } + } + + private void verifyDisplayedBolusAmount(RuffyScripter scripter) { + double displayedBolus = (double) scripter.currentMenu.getAttribute(MenuAttribute.BOLUS); + log.debug("Final bolus: " + displayedBolus); + // TODO can't we just use BigDecimal? doubles aren't precise ... + if (Math.abs(displayedBolus - bolus) > 0.001) { + throw new CommandException().message("Failed to set correct bolus. Expected: " + bolus + ", actual: " + displayedBolus); + } + } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/CancelTbrCommand.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/CancelTbrCommand.java new file mode 100644 index 0000000000..40baeab2a6 --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/CancelTbrCommand.java @@ -0,0 +1,20 @@ +package de.jotomo.ruffyscripter.commands; + +import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute; +import org.monkey.d.ruffy.ruffy.driver.display.MenuType; + +import de.jotomo.ruffyscripter.RuffyScripter; + +// TODO robustness: can a TBR run out, whilst we're trying to cancel it? +public class CancelTbrCommand implements Command { + @Override + public CommandResult execute(RuffyScripter scripter) { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + Double tbrPercentage = (Double) scripter.currentMenu.getAttribute(MenuAttribute.TBR); + boolean runtimeDisplayed = scripter.currentMenu.attributes().contains(MenuAttribute.RUNTIME); + if (tbrPercentage == 100 && !runtimeDisplayed) { + return new CommandResult().success(true).enacted(false).message("No TBR active"); + } + return new SetTbrCommand(100, 0).execute(scripter); + } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/Command.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/Command.java new file mode 100644 index 0000000000..c70384ae36 --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/Command.java @@ -0,0 +1,11 @@ +package de.jotomo.ruffyscripter.commands; + +import de.jotomo.ruffyscripter.RuffyScripter; + +public interface Command { + CommandResult execute(RuffyScripter ruffyScripter); + +// default String toString() { +// return getClass().getSimpleName(); +// } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandException.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandException.java new file mode 100644 index 0000000000..4e6637f84f --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandException.java @@ -0,0 +1,44 @@ +package de.jotomo.ruffyscripter.commands; + +public class CommandException extends RuntimeException { + public boolean success = false; + public boolean enacted = false; + public Exception exception = null; + public String message = null; + + public CommandException() {} + + public CommandException success(boolean success) { + this.success = success; + return this; + } + + public CommandException enacted(boolean enacted) { + this.enacted = enacted; + return this; + } + + public CommandException exception(Exception exception) { + this.exception = exception; + return this; + } + + public CommandException message(String message) { + this.message = message; + return this; + } + + public CommandResult toCommandResult() { + return new CommandResult().success(success).enacted(enacted).exception(exception).message(message); + } + + @Override + public String toString() { + return "CommandException{" + + "success=" + success + + ", enacted=" + enacted + + ", exception=" + exception + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandResult.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandResult.java new file mode 100644 index 0000000000..0a78f2a147 --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/CommandResult.java @@ -0,0 +1,41 @@ +package de.jotomo.ruffyscripter.commands; + +public class CommandResult { + public boolean success; + public boolean enacted; + public Exception exception; + public String message; + + public CommandResult() { + } + + public CommandResult success(boolean success) { + this.success = success; + return this; + } + + public CommandResult enacted(boolean enacted) { + this.enacted = enacted; + return this; + } + + public CommandResult exception(Exception exception) { + this.exception = exception; + return this; + } + + public CommandResult message(String message) { + this.message = message; + return this; + } + + @Override + public String toString() { + return "CommandResult{" + + "success=" + success + + ", enacted=" + enacted + + ", exception=" + exception + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/app/src/main/java/de/jotomo/ruffyscripter/commands/SetTbrCommand.java b/app/src/main/java/de/jotomo/ruffyscripter/commands/SetTbrCommand.java new file mode 100644 index 0000000000..25a02229f9 --- /dev/null +++ b/app/src/main/java/de/jotomo/ruffyscripter/commands/SetTbrCommand.java @@ -0,0 +1,238 @@ +package de.jotomo.ruffyscripter.commands; + +import android.os.SystemClock; + +import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute; +import org.monkey.d.ruffy.ruffy.driver.display.MenuType; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +import de.jotomo.ruffyscripter.RuffyScripter; + +public class SetTbrCommand implements Command { + private static final Logger log = LoggerFactory.getLogger(SetTbrCommand.class); + + private final long percentage; + private final long duration; + + public SetTbrCommand(long percentage, long duration) { + this.percentage = percentage; + this.duration = duration; + + if (percentage % 10 != 0) { + throw new IllegalArgumentException("TBR percentage must be set in 10% steps"); + } + if (percentage < 0 || percentage > 500) { + throw new IllegalArgumentException("TBR percentage must be within 0-500%"); + } + + if (percentage != 100) { + if (duration % 15 != 0) { + throw new IllegalArgumentException("TBR duration can only be set in 15 minute steps"); + } + if (duration > 60 * 24) { + throw new IllegalArgumentException("Maximum TBR duration is 24 hours"); + } + } + + if (percentage == 0 && duration > 120) { + throw new IllegalArgumentException("Max allowed zero-temp duration is 2h"); + } + } + + @Override + public CommandResult execute(RuffyScripter scripter) { + try { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + enterTbrMenu(scripter); + inputTbrPercentage(scripter); + SystemClock.sleep(500); + verifyDisplayedTbrPercentage(scripter); + + if (percentage == 100) { + cancelTbrAndConfirmCancellationWarning(scripter); + } else { + // switch to TBR_DURATION menu by pressing menu key + scripter.pressMenuKey(); + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + + inputTbrDuration(scripter); + SystemClock.sleep(500); + verifyDisplayedTbrDuration(scripter); + + // confirm TBR + scripter.pressCheckKey(); + SystemClock.sleep(500); + } + + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU, + "Pump did not return to MAIN_MEU after setting TBR. " + + "Check pump manually, the TBR might not have been set/cancelled."); + + // check main menu shows the same values we just set + if (percentage == 100) { + verifyMainMenuShowsNoActiveTbr(scripter); + return new CommandResult().success(true).enacted(true).message("TBR was cancelled"); + } else { + verifyMainMenuShowsExpectedTbrActive(scripter); + return new CommandResult().success(true).enacted(true).message( + String.format(Locale.US, "TBR set to %d%% for %d min", percentage, duration)); + } + + } catch (CommandException e) { + return e.toCommandResult(); + } + } + + private void enterTbrMenu(RuffyScripter scripter) { + scripter.navigateToMenu(MenuType.TBR_MENU); + scripter.pressCheckKey(); + scripter.waitForMenuUpdate(); + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + } + + private void inputTbrPercentage(RuffyScripter scripter) { + long currentPercent = readDisplayedTbrPercentage(scripter); + log.debug("Current TBR %: " + currentPercent); + long percentageChange = percentage - currentPercent; + long percentageSteps = percentageChange / 10; + boolean increasePercentage = true; + if (percentageSteps < 0) { + increasePercentage = false; + percentageSteps = Math.abs(percentageSteps); + } + log.debug("Pressing " + (increasePercentage ? "up" : "down") + " " + percentageSteps + " times"); + for (int i = 0; i < percentageSteps; i++) { + if (increasePercentage) scripter.pressUpKey(); + else scripter.pressDownKey(); + // TODO waitForMenuChange instead // or have key press method handle that?? + SystemClock.sleep(100); + log.debug("Push #" + (i + 1)); + } + } + + private void verifyDisplayedTbrPercentage(RuffyScripter scripter) { + long displayedPercentage = readDisplayedTbrPercentage(scripter); + if (displayedPercentage != this.percentage) { + log.debug("Final displayed TBR percentage: " + displayedPercentage); + throw new CommandException().message("Failed to set TBR percentage"); + } + } + + private long readDisplayedTbrPercentage(RuffyScripter scripter) { + Object percentageObj = scripter.currentMenu.getAttribute(MenuAttribute.BASAL_RATE); + // this as a bit hacky, the display value is blinking, so we might catch that, so + // keep trying till we get the Double we want + while (!(percentageObj instanceof Double)) { + scripter.waitForMenuUpdate(); + percentageObj = scripter.currentMenu.getAttribute(MenuAttribute.BASAL_RATE); + } + return ((Double) percentageObj).longValue(); + } + + private void inputTbrDuration(RuffyScripter scripter) { + long currentDuration = readDisplayedTbrDuration(scripter); + if (currentDuration % 15 != 0) { + // The duration displayed is how long an active TBR will still run, + // which might be something like 0:43, hence not in 15 minute steps. + // Pressing down will go to the next lower 15 minute step. + scripter.pressDownKey(); + scripter.waitForMenuUpdate(); + currentDuration = readDisplayedTbrDuration(scripter); + } + log.debug("Current TBR duration: " + currentDuration); + long durationChange = duration - currentDuration; + long durationSteps = durationChange / 15; + boolean increaseDuration = true; + if (durationSteps < 0) { + increaseDuration = false; + durationSteps = Math.abs(durationSteps); + } + log.debug("Pressing " + (increaseDuration ? "up" : "down") + " " + durationSteps + " times"); + for (int i = 0; i < durationSteps; i++) { + if (increaseDuration) scripter.pressUpKey(); + else scripter.pressDownKey(); + // TODO waitForMenuChange instead // or have key press method handle that?? + SystemClock.sleep(100); + log.debug("Push #" + (i + 1)); + } + } + + private void verifyDisplayedTbrDuration(RuffyScripter scripter) { + long displayedDuration = readDisplayedTbrDuration(scripter); + if (displayedDuration != duration) { + log.debug("Final displayed TBR duration: " + displayedDuration); + throw new CommandException().message("Failed to set TBR duration"); + } + } + + private long readDisplayedTbrDuration(RuffyScripter scripter) { + Object durationObj = scripter.currentMenu.getAttribute(MenuAttribute.RUNTIME); + // this as a bit hacky, the display value is blinking, so we might catch that, so + // keep trying till we get the Double we want + while (!(durationObj instanceof MenuTime)) { + scripter.waitForMenuUpdate(); + durationObj = scripter.currentMenu.getAttribute(MenuAttribute.RUNTIME); + } + MenuTime duration = (MenuTime) durationObj; + return duration.getHour() * 60 + duration.getMinute(); + } + + private void cancelTbrAndConfirmCancellationWarning(RuffyScripter scripter) { + // TODO this will fail if no TBR is running; detect and just throw CE(success=true, msg="nothing to do")? + + // confirm entered TBR + scripter.pressCheckKey(); + scripter.waitForMenuUpdate(); + + // hm, waiting here (more) makes things worse, if we don't press the alert away quickly, + // the pump exits BT mode ... so I guess we'll live with the checks below, + // verifying we made it back to the main menu and the displayed TBR data + // corresponds to what we set. Hope the timing is stable enough ... + +/* scripter.waitForMenuToBeLeft(MenuType.TBR_SET); + if (scripter.currentMenu.getType() != MenuType.MAIN_MENU) { + // pump shortly enters the main menu before raising the alert + // TODO is this always entered? + log.debug("TBR cancelled, going over main menu"); + scripter.waitForMenuToBeLeft(MenuType.MAIN_MENU); + } + if (scripter.currentMenu.getType() != MenuType.WARNING_OR_ERROR) { + throw new CommandException(false, null, "Expected WARNING_OR_ERROR menu was not shown when cancelling TBR"); + }*/ + // confirm "TBR cancelled alert" + scripter.pressCheckKey(); + SystemClock.sleep(200); + // dismiss "TBR cancelled alert" + scripter.pressCheckKey(); + scripter.waitForMenuToBeLeft(MenuType.WARNING_OR_ERROR); + } + + private void verifyMainMenuShowsNoActiveTbr(RuffyScripter scripter) { + Double tbrPercentage = (Double) scripter.currentMenu.getAttribute(MenuAttribute.TBR); + boolean runtimeDisplayed = scripter.currentMenu.attributes().contains(MenuAttribute.RUNTIME); + if (tbrPercentage != 100 || runtimeDisplayed) { + throw new CommandException().message("Cancelling TBR failed, TBR is still set according to MAIN_MENU"); + } + } + + private void verifyMainMenuShowsExpectedTbrActive(RuffyScripter scripter) { + // new TBR set; percentage and duration must be displayed ... + if (!scripter.currentMenu.attributes().contains(MenuAttribute.TBR) || + !scripter.currentMenu.attributes().contains(MenuAttribute.TBR)) { + throw new CommandException().message("Setting TBR failed, according to MAIN_MENU no TBR is active"); + } + Double mmTbrPercentage = (Double) scripter.currentMenu.getAttribute(MenuAttribute.TBR); + MenuTime mmTbrDuration = (MenuTime) scripter.currentMenu.getAttribute(MenuAttribute.RUNTIME); + // ... and be the same as what we set + // note that displayed duration might have already counted down, e.g. from 30 minutes to + // 29 minutes and 59 seconds, so that 29 minutes are displayed + int mmTbrDurationInMinutes = mmTbrDuration.getHour() * 60 + mmTbrDuration.getMinute(); + if (mmTbrPercentage != percentage || (mmTbrDurationInMinutes != duration && mmTbrDurationInMinutes + 1 != duration)) { + throw new CommandException().message("Setting TBR failed, TBR in MAIN_MENU differs from expected"); + } + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java new file mode 100644 index 0000000000..20e74c7805 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java @@ -0,0 +1,123 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import org.monkey.d.ruffy.ruffy.driver.display.menu.BolusType; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuBlink; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuDate; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Created by fishermen21 on 20.05.17. + */ + +public class Menu implements Parcelable{ + private MenuType type; + private Map attributes = new HashMap<>(); + + public Menu(MenuType type) + { + this.type = type; + } + + public Menu(Parcel in) { + this.type = MenuType.valueOf(in.readString()); + while(in.dataAvail()>0) { + try { + String attr = in.readString(); + String clas = in.readString(); + String value = in.readString(); + + MenuAttribute a = MenuAttribute.valueOf(attr); + Object o = null; + if (Integer.class.toString().equals(clas)) { + o = new Integer(value); + } else if (Double.class.toString().equals(clas)) { + o = new Double(value); + } else if (Boolean.class.toString().equals(clas)) { + o = new Boolean(value); + } else if (MenuDate.class.toString().equals(clas)) { + o = new MenuDate(value); + } else if (MenuTime.class.toString().equals(clas)) { + o = new MenuTime(value); + } else if (MenuBlink.class.toString().equals(clas)) { + o = new MenuBlink(); + } else if (BolusType.class.toString().equals(clas)) { + o = BolusType.valueOf(value); + } else if (String.class.toString().equals(clas)) { + o = new String(value); + } + + if (o != null) { + attributes.put(a, o); + } else { + Log.e("MenuIn", "failed to parse: " + attr + " / " + clas + " / " + value); + } + }catch(Exception e) + { + Log.e("MenuIn","Exception in read",e); + } + + } + } + + public void setAttribute(MenuAttribute key, Object value) + { + attributes.put(key,value); + } + + public List attributes() + { + return new LinkedList(attributes.keySet()); + } + + public Object getAttribute(MenuAttribute key) + { + return attributes.get(key); + } + + public MenuType getType() { + return type; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type.toString()); + for(MenuAttribute a : attributes.keySet()) + { + try + { + dest.writeString(a.toString()); + Object o = attributes.get(a); + + dest.writeString(o.getClass().toString()); + dest.writeString(o.toString()); + }catch(Exception e) + { + Log.v("MenuOut","error in write",e); + } + } + } + public static final Creator CREATOR = new + Creator() { + public Menu createFromParcel(Parcel in) { + return new Menu(in); + } + + public Menu[] newArray(int size) { + return new Menu[size]; + } + }; +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java new file mode 100644 index 0000000000..6176a2be54 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java @@ -0,0 +1,34 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum MenuAttribute { + RUNTIME,//runtime of current operation, remaining time on main menu + BOLUS,//double units + BOLUS_REMAINING,//double units remain from current bolus + TBR,//double 0-500% + BASAL_RATE,//double units/h + BASAL_SELECTED,//int selected basal profile + LOW_BATTERY,//boolean low battery warning + INSULIN_STATE,//int insulin warning 0 == no warning, 1== low, 2 == empty + LOCK_STATE,//int keylock state 0==no lock, 1==unlocked, 2==locked + MULTIWAVE_BOLUS,//double immediate bolus on multiwave + BOLUS_TYPE,//BolusType, only history uses MULTIWAVE + TIME,//time MenuTime + REMAINING_INSULIN,//double units + DATE,//date MenuDate + CURRENT_RECORD,//int current record + TOTAL_RECORD, //int total num record + ERROR, //int errorcode + WARNING, //int errorcode + MESSAGE, //string errormessage + DAILY_TOTAL, //double units + BASAL_TOTAL, //double total basal + BASAL_START, //time MenuTime the basalrate starts + BASAL_END, // time MenuTime the basalrate ends + DEBUG_TIMING, //double with timing infos + WARANTY, //boolean true if out of waranty + ERROR_OR_WARNING, // set if menu in blink during error/warning +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java new file mode 100644 index 0000000000..4d00ea9155 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java @@ -0,0 +1,42 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum MenuType { + MAIN_MENU, + STOP_MENU, + BOLUS_MENU, + BOLUS_ENTER, + EXTENDED_BOLUS_MENU, + BOLUS_DURATION, + MULTIWAVE_BOLUS_MENU, + IMMEDIATE_BOLUS, + TBR_MENU, + MY_DATA_MENU, + BASAL_MENU, + BASAL_1_MENU, + BASAL_2_MENU, + BASAL_3_MENU, + BASAL_4_MENU, + BASAL_5_MENU, + DATE_AND_TIME_MENU, + ALARM_MENU, + MENU_SETTINGS_MENU, + BLUETOOTH_MENU, + THERAPY_MENU, + PUMP_MENU, + QUICK_INFO, + BOLUS_DATA, + DAILY_DATA, + TBR_DATA, + ERROR_DATA, + TBR_SET, + TBR_DURATION, + STOP, + START_MENU, + BASAL_TOTAL, + BASAL_SET, + WARNING_OR_ERROR, +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java new file mode 100644 index 0000000000..82572acc93 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java @@ -0,0 +1,13 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum BolusType{ + NORMAL, + EXTENDED, + MULTIWAVE, + MULTIWAVE_BOLUS, + MULTIWAVE_EXTENDED, +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java new file mode 100644 index 0000000000..18c0374dcb --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java @@ -0,0 +1,12 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public class MenuBlink { + @Override + public String toString() { + return "BLINK"; + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java new file mode 100644 index 0000000000..9b727cc965 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java @@ -0,0 +1,27 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 24.05.17. + */ + +public class MenuDate { + private final int day; + private final int month; + + + public MenuDate(int day, int month) { + this.day = day; + this.month = month; + } + + public MenuDate(String value) { + String[] p = value.split("\\."); + day = Integer.parseInt(p[0]); + month = Integer.parseInt(p[1]); + } + + @Override + public String toString() { + return day+"."+String.format("%02d",month)+"."; + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java new file mode 100644 index 0000000000..147aafc8eb --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java @@ -0,0 +1,36 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public class MenuTime { + + private final int hour; + private final int minute; + + public MenuTime(int hour, int minute) + { + this.hour = hour; + this.minute = minute; + } + + public MenuTime(String value) { + String[] p = value.split(":"); + hour = Integer.parseInt(p[0]); + minute = Integer.parseInt(p[1]); + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + @Override + public String toString() { + return hour+":"+String.format("%02d",minute); + } +}