Merge branch 'stable' into '1_5f'

# Conflicts:
#   app/src/main/res/values/strings.xml
This commit is contained in:
AdrianLxM 2017-07-29 16:55:55 +00:00
commit bfd96b0635
35 changed files with 2875 additions and 5 deletions

View file

@ -172,6 +172,9 @@ dependencies {
androidTestCompile 'org.mockito:mockito-core:2.7.22' androidTestCompile 'org.mockito:mockito-core:2.7.22'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile(name: 'android-edittext-validator-v1.3.4-mod', ext: 'aar') compile(name: 'android-edittext-validator-v1.3.4-mod', ext: 'aar')
compile ('com.google.android:flexbox:0.3.0') { compile ('com.google.android:flexbox:0.3.0') {
exclude group: 'com.android.support' exclude group: 'com.android.support'
@ -182,5 +185,4 @@ dependencies {
} }
compile 'com.google.code.gson:gson:2.7' compile 'com.google.code.gson:gson:2.7'
compile 'com.google.guava:guava:20.0' compile 'com.google.guava:guava:20.0'
} }

View file

@ -0,0 +1,107 @@
package ruffyscripter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.monkey.d.ruffy.ruffy.driver.IRuffyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.jotomo.ruffyscripter.RuffyScripter;
import de.jotomo.ruffyscripter.commands.CommandResult;
import de.jotomo.ruffyscripter.commands.ReadPumpStateCommand;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/* Timing notes
Bolusing 15 U: input 15 U (150x button up): ~40s
pump wait after confirm : ~5s
delivering : ~75s
total : ~120s
*/
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class RuffyScripterInstrumentedTest {
private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class);
private static Context appContext = InstrumentationRegistry.getTargetContext();
private static ServiceConnection mRuffyServiceConnection;
private static RuffyScripter ruffyScripter;
@BeforeClass
public static void bindRuffy() {
Intent intent = new Intent()
.setComponent(new ComponentName(
// this must be the base package of the app (check package attribute in
// manifest element in the manifest file of the providing app)
"org.monkey.d.ruffy.ruffy",
// full path to the driver
// in the logs this service is mentioned as (note the slash)
// "org.monkey.d.ruffy.ruffy/.driver.Ruffy"
"org.monkey.d.ruffy.ruffy.driver.Ruffy"
));
appContext.startService(intent);
mRuffyServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ruffyScripter = new RuffyScripter(IRuffyService.Stub.asInterface(service));
log.debug("ruffy serivce connected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
log.debug("ruffy service disconnected");
}
};
boolean success = appContext.bindService(intent, mRuffyServiceConnection, Context.BIND_AUTO_CREATE);
if (!success) {
log.error("Binding to ruffy service failed");
}
long timeout = System.currentTimeMillis() + 60 * 1000;
while (ruffyScripter == null) {
SystemClock.sleep(500);
log.debug("Waiting for ruffy service connection");
long now = System.currentTimeMillis();
if (now > timeout) {
throw new RuntimeException("Ruffy service could not be bound");
}
}
}
@AfterClass
public static void unbindRuffy() {
appContext.unbindService(mRuffyServiceConnection);
}
// TODO now, how to get ruffy fired up in this test?
@Test
public void readPumpState() throws Exception {
CommandResult commandResult = ruffyScripter.runCommand(new ReadPumpStateCommand());
assertTrue(commandResult.success);
assertFalse(commandResult.enacted);
assertNotNull(commandResult.state);
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -0,0 +1,3 @@
package org.monkey.d.ruffy.ruffy.driver.display;
parcelable Menu;

View file

@ -15,7 +15,7 @@
<maxHistory>120</maxHistory> <maxHistory>120</maxHistory>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%file:%line]: %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
@ -24,7 +24,7 @@
<pattern>%logger{0}</pattern> <pattern>%logger{0}</pattern>
</tagEncoder> </tagEncoder>
<encoder> <encoder>
<pattern>[%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>[%thread] %-5level [%file:%line]: %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>

View file

@ -0,0 +1,5 @@
package de.jotomo.ruffyscripter;
/** The history data read from "My data" */
public class History {
}

View file

@ -0,0 +1,65 @@
package de.jotomo.ruffyscripter;
import java.util.Date;
/**
* State representing the state of the MAIN_MENU.
*/
public class PumpState {
public Date timestamp = new Date();
public boolean tbrActive = false;
public int tbrPercent = -1;
public double tbrRate = -1;
public int tbrRemainingDuration = -1;
/** This is the error message (if any) displayed by the pump if there is an alarm,
e.g. if a "TBR cancelled alarm" is active, the value will be "TBR CANCELLED".
Generally, an error code is also displayed, but it flashes and it might take
longer to read that and the pump connection gets interrupted if we're not
reacting quickly.
*/
public String errorMsg;
public boolean suspended;
public PumpState tbrActive(boolean tbrActive) {
this.tbrActive = tbrActive;
return this;
}
public PumpState tbrPercent(int tbrPercent) {
this.tbrPercent = tbrPercent;
return this;
}
public PumpState tbrRate(double tbrRate) {
this.tbrRate = tbrRate;
return this;
}
public PumpState tbrRemainingDuration(int tbrRemainingDuration) {
this.tbrRemainingDuration = tbrRemainingDuration;
return this;
}
public PumpState errorMsg(String errorMsg) {
this.errorMsg = errorMsg;
return this;
}
public PumpState suspended(boolean suspended) {
this.suspended = suspended;
return this;
}
@Override
public String toString() {
return "PumpState{" +
"tbrActive=" + tbrActive +
", tbrPercent=" + tbrPercent +
", tbrRate=" + tbrRate +
", tbrRemainingDuration=" + tbrRemainingDuration +
", errorMsg=" + errorMsg +
", suspended=" + suspended +
", timestamp=" + timestamp +
'}';
}
}

View file

@ -0,0 +1,481 @@
package de.jotomo.ruffyscripter;
import android.os.DeadObjectException;
import android.os.RemoteException;
import android.os.SystemClock;
import com.google.common.base.Joiner;
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.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.List;
import de.jotomo.ruffyscripter.commands.Command;
import de.jotomo.ruffyscripter.commands.CommandException;
import de.jotomo.ruffyscripter.commands.CommandResult;
import de.jotomo.ruffyscripter.commands.ReadPumpStateCommand;
// 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);
private final IRuffyService ruffyService;
private final long connectionTimeOutMs = 5000;
private String unrecoverableError = null;
public volatile Menu currentMenu;
private volatile long menuLastUpdated = 0;
private volatile long lastCmdExecutionTime;
private volatile Command activeCmd = null;
private volatile boolean connected = false;
private volatile long lastDisconnected = 0;
public RuffyScripter(final IRuffyService ruffyService) {
this.ruffyService = ruffyService;
try {
ruffyService.setHandler(mHandler);
idleDisconnectMonitorThread.start();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
private Thread idleDisconnectMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
long lastDisconnect = System.currentTimeMillis();
while (unrecoverableError == null) {
try {
long now = System.currentTimeMillis();
if (connected && activeCmd == null
&& now > lastCmdExecutionTime + connectionTimeOutMs
// don't disconnect too frequently, confuses ruffy?
&& now > lastDisconnect + 15 * 1000) {
log.debug("Disconnecting after " + (connectionTimeOutMs / 1000) + "s inactivity timeout");
lastDisconnect = now;
ruffyService.doRTDisconnect();
connected = false;
lastDisconnect = System.currentTimeMillis();
// don't attempt anything fancy in the next 10s, let the pump settle
SystemClock.sleep(10 * 1000);
}
} catch (DeadObjectException doe) {
// TODO do we need to catch this exception somewhere else too? right now it's
// converted into a command failure, but it's not classified as unrecoverable;
// eventually we might try to recover ... check docs, there's also another
// execption we should watch interacting with a remote service.
// SecurityException was the other, when there's an AIDL mismatch;
unrecoverableError = "Ruffy service went away";
} catch (RemoteException e) {
log.debug("Exception in idle disconnect monitor thread, carrying on", e);
}
SystemClock.sleep(1000);
}
}
}, "idle-disconnect-monitor");
private IRTHandler mHandler = new IRTHandler.Stub() {
@Override
public void log(String message) throws RemoteException {
log.trace("Ruffy says: " + message);
}
@Override
public void fail(String message) throws RemoteException {
log.warn("Ruffy warns: " + 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;
connected = false;
}
@Override
public void rtStarted() throws RemoteException {
log.debug("rtStarted callback invoked");
connected = true;
}
@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();
connected = true;
// 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;
}
private static class Returnable {
CommandResult cmdResult;
}
/**
* Always returns a CommandResult, never throws
*/
public CommandResult runCommand(final Command cmd) {
if (unrecoverableError != null) {
return new CommandResult().success(false).enacted(false).message(unrecoverableError);
}
List<String> violations = cmd.validateArguments();
if (!violations.isEmpty()) {
return new CommandResult().message(Joiner.on("\n").join(violations)).state(readPumpState());
}
synchronized (RuffyScripter.class) {
try {
activeCmd = cmd;
ensureConnected();
final RuffyScripter scripter = this;
final Returnable returnable = new Returnable();
Thread cmdThread = new Thread(new Runnable() {
@Override
public void run() {
try {
// check if pump is an an error state
if (currentMenu == null || currentMenu.getType() == MenuType.WARNING_OR_ERROR) {
try {
returnable.cmdResult = new CommandResult().message("Pump is in an error state: " + currentMenu.getAttribute(MenuAttribute.MESSAGE));
return;
} catch (Exception e) {
returnable.cmdResult = new CommandResult().message("Pump is in an error state, reading the error state resulted in the attached exception").exception(e);
return;
}
}
// Except for ReadPumpStateCommand: fail on all requests if the pump is suspended.
// All trickery of not executing but returning success, so that AAPS can non-sensically TBR away when suspended
// are dangerous in the current model where commands are dispatched without checking state beforehand, so
// the above tactic would result in boluses not being applied and no warning being raised.
// (Doing this check on the ComboPlugin level would require the plugin to fetch state from the pump,
// deal with states changes (running/stopped), propagating that to AAPS and so on, adding more state,
// which adds complexity I don't want in v1 and which requires more up-front design to do well,
// esp. with AAPS).
// So, for v1, just check the pump is not suspended before executing commands and raising an error for all
// but the ReadPumpStateCommand. For v2, we'll have to come up with a better idea how to deal with the pump's
// state. Maybe having read-only commands and write/treatment commands treated differently, or maybe
// build an abstraction on top of the commands, so that e.g. a method on RuffyScripter encapsulates checking
// pre-condititions, running one or several commands, checking-post conditions and what not.
// Or maybe stick with commands, have them specify if they can run in stop mode. Think properly at which
// level to handle state and logic.
// For now, when changing cartridges and such: tell AAPS to stop the loop, change cartridge and resume the loop.
if (currentMenu == null || currentMenu.getType() == MenuType.STOP) {
if (cmd instanceof ReadPumpStateCommand) {
returnable.cmdResult = new CommandResult().success(true).enacted(false);
} else {
returnable.cmdResult = new CommandResult().success(false).enacted(false).message("Pump is suspended");
}
return;
}
log.debug("Connection ready to execute cmd " + cmd);
PumpState pumpState = readPumpState();
log.debug("Pump state before running command: " + pumpState);
long cmdStartTime = System.currentTimeMillis();
returnable.cmdResult = cmd.execute(scripter, pumpState);
long cmdEndTime = System.currentTimeMillis();
returnable.cmdResult.completionTime = cmdEndTime;
log.debug("Executing " + cmd + " took " + (cmdEndTime - cmdStartTime) + "ms");
} catch (CommandException e) {
returnable.cmdResult = e.toCommandResult();
} catch (Exception e) {
log.error("Unexpected exception running cmd", e);
returnable.cmdResult = new CommandResult().exception(e).message("Unexpected exception running cmd");
} finally {
lastCmdExecutionTime = System.currentTimeMillis();
}
}
}, cmd.toString());
cmdThread.start();
// time out if nothing has been happening for more than 90s or after 4m
// (to fail before the next loop iteration issues the next command)
long dynamicTimeout = System.currentTimeMillis() + 90 * 1000;
long overallTimeout = System.currentTimeMillis() + 4 * 60 * 1000;
while (cmdThread.isAlive()) {
log.trace("Waiting for running command to complete");
SystemClock.sleep(500);
long now = System.currentTimeMillis();
if (now > dynamicTimeout) {
boolean menuRecentlyUpdated = now < menuLastUpdated + 5 * 1000;
boolean inMenuNotMainMenu = currentMenu != null && currentMenu.getType() != MenuType.MAIN_MENU;
if (menuRecentlyUpdated || inMenuNotMainMenu) {
// command still working (or waiting for pump to complete, e.g. bolus delivery)
dynamicTimeout = now + 30 * 1000;
} else {
log.error("Dynamic timeout running command " + activeCmd);
cmdThread.interrupt();
SystemClock.sleep(5000);
log.error("Timed out thread dead yet? " + cmdThread.isAlive());
return new CommandResult().success(false).enacted(false).message("Command stalled, check pump!");
}
}
if (now > overallTimeout) {
String msg = "Command " + cmd + " timed out after 4 min, check pump!";
log.error(msg);
return new CommandResult().success(false).enacted(false).message(msg);
}
}
if (returnable.cmdResult.state == null) {
returnable.cmdResult.state = readPumpState();
}
log.debug("Command result: " + returnable.cmdResult);
return returnable.cmdResult;
} catch (CommandException e) {
return e.toCommandResult();
} catch (Exception e) {
log.error("Error in ruffyscripter/ruffy", e);
return new CommandResult().exception(e).message("Unexpected exception communication with ruffy: " + e.getMessage());
} finally {
activeCmd = null;
}
}
}
/** If there's an issue, this times out eventually and throws a CommandException */
private void ensureConnected() {
try {
boolean menuUpdateRecentlyReceived = currentMenu != null && menuLastUpdated + 1000 > System.currentTimeMillis();
log.debug("ensureConnect, connected: " + connected + ", receiving menu updates: " + menuUpdateRecentlyReceived);
if (menuUpdateRecentlyReceived) {
log.debug("Pump is sending us menu updates, so we're connected");
return;
}
// Occasionally the rtConnect is called a few seconds after the rtDisconnected
// callback was called, in response to your disconnect request via doRtDisconnect.
// When connecting again shortly after disconnecting, the pump sometimes fails
// to come up. So for v1, just wait. This happens rarely, so no overly fancy logic needed.
// TODO v2 see if we can do this cleaner, use isDisconnected as well maybe. GL#34.
if (System.currentTimeMillis() < lastDisconnected + 10 * 1000) {
log.debug("Waiting 10s to let pump settle after recent disconnect");
SystemClock.sleep(10 * 1000);
}
boolean connectInitSuccessful = ruffyService.doRTConnect() == 0;
log.debug("Connect init successful: " + connectInitSuccessful);
log.debug("Waiting for first menu update to be sent");
// Note: there was an 'if(currentMenu == null)' around the next call, since
// the rtDisconnected callback sets currentMenu = null. However, there were
// race conditions, so it was removed. And really, waiting for an update
// to come in is a much safer bet.
// waitForMenuUpdate times out after 60s and throws a CommandException.
// if the user just pressed a button on the combo, the screen needs to time first
// before a connection is possible. In that case, it takes 45s before the
// connection comes up.
waitForMenuUpdate(90);
} catch (RemoteException e) {
throw new CommandException().exception(e).message("Unexpected exception while initiating/restoring pump connection");
}
}
// below: methods to be used by commands
// TODO move into a new Operations(scripter) class commands can delegate to,
// so this class can focus on providing a connection to run commands
// (or maybe reconsider putting it into a base class)
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() {
log.debug("Pressing up key");
pressKey(Key.UP);
log.debug("Releasing up key");
}
public void pressDownKey() {
log.debug("Pressing down key");
pressKey(Key.DOWN);
log.debug("Releasing down key");
}
public void pressCheckKey() {
log.debug("Pressing check key");
pressKey(Key.CHECK);
log.debug("Releasing check key");
}
public void pressMenuKey() {
log.debug("Pressing menu key");
pressKey(Key.MENU);
log.debug("Releasing menu key");
}
// TODO v2, rework these two methods: waitForMenuUpdate shoud only be used by commands
// then anything longer than a few seconds is an error;
// only ensureConnected() uses the method with the timeout parameter; inline that code,
// so we can use a custom timeout and give a better error message in case of failure
/**
* Wait until the menu update is in
*/
public void waitForMenuUpdate() {
waitForMenuUpdate(60);
}
public void waitForMenuUpdate(long timeoutInSeconds) {
long timeoutExpired = System.currentTimeMillis() + timeoutInSeconds * 1000;
long initialUpdateTime = menuLastUpdated;
while (initialUpdateTime == menuLastUpdated) {
if (System.currentTimeMillis() > timeoutExpired) {
throw new CommandException().message("Timeout waiting for menu update");
}
SystemClock.sleep(50);
}
}
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) {
MenuType startedFrom = currentMenu.getType();
boolean movedOnce = false;
while (currentMenu.getType() != desiredMenu) {
MenuType currentMenuType = currentMenu.getType();
log.debug("Navigating to menu " + desiredMenu + ", currenty menu: " + currentMenuType);
if (movedOnce && currentMenuType == startedFrom) {
throw new CommandException().message("Menu not found searching for " + desiredMenu
+ ". Check menu settings on your pump to ensure it's not hidden.");
}
pressMenuKey();
waitForMenuToBeLeft(currentMenuType);
movedOnce = true;
}
}
/**
* Wait till a menu changed has completed, "away" from the menu provided as argument.
*/
public void waitForMenuToBeLeft(MenuType menuType) {
long timeout = System.currentTimeMillis() + 60 * 1000;
while (currentMenu.getType() == menuType) {
if (System.currentTimeMillis() > timeout) {
throw new CommandException().message("Timeout waiting for menu " + menuType + " to be left");
}
SystemClock.sleep(10);
}
}
public void verifyMenuIsDisplayed(MenuType expectedMenu) {
verifyMenuIsDisplayed(expectedMenu, null);
}
public void verifyMenuIsDisplayed(MenuType expectedMenu, String failureMessage) {
int retries = 600;
while (currentMenu.getType() != expectedMenu) {
if (retries > 0) {
SystemClock.sleep(100);
retries = retries - 1;
} else {
if (failureMessage == null) {
failureMessage = "Invalid pump state, expected to be in menu " + expectedMenu + ", but current menu is " + currentMenu.getType();
}
throw new CommandException().message(failureMessage);
}
}
}
// TODO v2 add remaining info we can extract from the main menu, low battery and low
// cartridge warnings, running extended bolus (how does that look if a TBR is active as well?)
private PumpState readPumpState() {
PumpState state = new PumpState();
Menu menu = currentMenu;
if (menu == null) {
return new PumpState().errorMsg("Menu is not available");
}
MenuType menuType = menu.getType();
if (menuType == MenuType.MAIN_MENU) {
Double tbrPercentage = (Double) menu.getAttribute(MenuAttribute.TBR);
if (tbrPercentage != 100) {
state.tbrActive = true;
Double displayedTbr = (Double) menu.getAttribute(MenuAttribute.TBR);
state.tbrPercent = displayedTbr.intValue();
MenuTime durationMenuTime = ((MenuTime) menu.getAttribute(MenuAttribute.RUNTIME));
state.tbrRemainingDuration = durationMenuTime.getHour() * 60 + durationMenuTime.getMinute();
state.tbrRate = ((double) menu.getAttribute(MenuAttribute.BASAL_RATE));
}
// TODO v2, read current base basal rate, which is shown center when no TBR is active.
// Check if that holds true when an extended bolus is running.
// Add a field to PumpStatus, rather than renaming/overloading tbrRate to mean
// either TBR rate or basal rate depending on whether a TBR is active.
} else if (menuType == MenuType.WARNING_OR_ERROR) {
state.errorMsg = (String) menu.getAttribute(MenuAttribute.MESSAGE);
} else if (menuType == MenuType.STOP) {
state.suspended = true;
} else {
StringBuilder sb = new StringBuilder();
for (MenuAttribute menuAttribute : menu.attributes()) {
sb.append(menuAttribute);
sb.append(": ");
sb.append(menu.getAttribute(menuAttribute));
sb.append("\n");
}
state.errorMsg = "Pump is on menu " + menuType + ", listing attributes: \n" + sb.toString();
}
return state;
}
}

View file

@ -0,0 +1,167 @@
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.ArrayList;
import java.util.List;
import java.util.Locale;
import de.jotomo.ruffyscripter.PumpState;
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 List<String> validateArguments() {
List<String> violations = new ArrayList<>();
if (bolus <= 0 || bolus > 25) {
violations.add("Requested bolus " + bolus + " out of limits (0-25)");
}
return violations;
}
@Override
public CommandResult execute(RuffyScripter scripter, PumpState initialPumpState) {
try {
enterBolusMenu(scripter);
inputBolusAmount(scripter);
verifyDisplayedBolusAmount(scripter);
// confirm bolus
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
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; the remaining units to deliver are counted
// down and are displayed on the main menu.
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.");
// read last bolus record; those menus display static data and therefore
// only a single menu update is sent
scripter.navigateToMenu(MenuType.MY_DATA_MENU);
scripter.verifyMenuIsDisplayed(MenuType.MY_DATA_MENU);
scripter.pressCheckKey();
if (scripter.currentMenu.getType() != MenuType.BOLUS_DATA) {
scripter.waitForMenuUpdate();
}
if (!scripter.currentMenu.attributes().contains(MenuAttribute.BOLUS)) {
throw new CommandException().success(false).enacted(true)
.message("Bolus was delivered, but unable to confirm it with history record");
}
double lastBolusInHistory = (double) scripter.currentMenu.getAttribute(MenuAttribute.BOLUS);
if (Math.abs(bolus - lastBolusInHistory) > 0.05) {
throw new CommandException().success(false).enacted(true)
.message("Last bolus shows " + lastBolusInHistory
+ " U delievered, but " + bolus + " U were requested");
}
log.debug("Bolus record in history confirms delivered bolus");
// leave menu to go back to main menu
scripter.pressCheckKey();
scripter.waitForMenuToBeLeft(MenuType.BOLUS_DATA);
scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU,
"Bolus was correctly delivered and checked against history, but we "
+ "did not return the main menu successfully.");
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.verifyMenuIsDisplayed(MenuType.MAIN_MENU);
scripter.navigateToMenu(MenuType.BOLUS_MENU);
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_MENU);
scripter.pressCheckKey();
scripter.waitForMenuUpdate();
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
}
private void inputBolusAmount(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
// press 'up' once for each 0.1 U increment
long steps = Math.round(bolus * 10);
for (int i = 0; i < steps; i++) {
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
scripter.pressUpKey();
SystemClock.sleep(100);
}
// Give the pump time to finish any scrolling that might still be going on, can take
// up to 1100ms. Plus some extra time to be sure
SystemClock.sleep(2000);
}
private void verifyDisplayedBolusAmount(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
double displayedBolus = readDisplayedBolusAmount(scripter);
log.debug("Final bolus: " + displayedBolus);
if (Math.abs(displayedBolus - bolus) > 0.05) {
throw new CommandException().message("Failed to set correct bolus. Expected: " + bolus + ", actual: " + displayedBolus);
}
// check again to ensure the displayed value hasn't change due to due scrolling taking extremely long
SystemClock.sleep(2000);
double refreshedDisplayedBolus = readDisplayedBolusAmount(scripter);
if (Math.abs(displayedBolus - refreshedDisplayedBolus) > 0.05) {
throw new CommandException().message("Failed to set bolus: bolus changed after input stopped from "
+ displayedBolus + " -> " + refreshedDisplayedBolus);
}
}
private double readDisplayedBolusAmount(RuffyScripter scripter) {
// TODO v2 add timeout? Currently the command execution timeout would trigger if exceeded
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER);
// bolus amount is blinking, so we need to make sure we catch it at the right moment
Object amountObj = scripter.currentMenu.getAttribute(MenuAttribute.BOLUS);
while (!(amountObj instanceof Double)) {
scripter.waitForMenuUpdate();
amountObj = scripter.currentMenu.getAttribute(MenuAttribute.BOLUS);
}
return (double) amountObj;
}
@Override
public String toString() {
return "BolusCommand{" +
"bolus=" + bolus +
'}';
}
}

View file

@ -0,0 +1,62 @@
package de.jotomo.ruffyscripter.commands;
import org.monkey.d.ruffy.ruffy.driver.display.MenuType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import de.jotomo.ruffyscripter.PumpState;
import de.jotomo.ruffyscripter.RuffyScripter;
import info.nightscout.androidaps.MainApp;
// TODO robustness: can a TBR run out, whilst we're trying to cancel it?
// Hm, we could just ignore TBRs that run out within the next 60s (0:01 or even 0:02
// given we need some time to process the request).
public class CancelTbrCommand implements Command {
private static final Logger log = LoggerFactory.getLogger(CancelTbrCommand.class);
@Override
public List<String> validateArguments() {
return Collections.emptyList();
}
@Override
public CommandResult execute(RuffyScripter scripter, PumpState initialPumpState) {
try {
scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU);
if (!initialPumpState.tbrActive) {
log.debug("active temp basal 90s ago: " +
MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis() - 90 * 1000));
log.debug("active temp basal 60s ago: " +
MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis() - 30 * 1000));
log.debug("active temp basal 30s ago: " +
MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis() - 30 * 1000));
log.debug("active temp basal now:: " +
MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis()));
// TODO keep checking logs to ensure this case only happens because CancelTbrCommand was called
// twice by AAPS
log.warn("No TBR active to cancel");
return new CommandResult()
.success(true)
// Technically, nothing was enacted, but AAPS needs this to recover
// when there was an issue and AAPS thinks a TBR is still active,
// so the ComboPlugin can create a TempporaryBasel to mark the TBR
// as finished to get in sync with the pump state.
.enacted(true)
.message("No TBR active");
}
log.debug("Cancelling active TBR of " + initialPumpState.tbrPercent
+ "% with " + initialPumpState.tbrRemainingDuration + " min remaining");
return new SetTbrCommand(100, 0).execute(scripter, initialPumpState);
} catch (CommandException e) {
return e.toCommandResult();
}
}
@Override
public String toString() {
return "CancelTbrCommand{}";
}
}

View file

@ -0,0 +1,18 @@
package de.jotomo.ruffyscripter.commands;
import java.util.List;
import de.jotomo.ruffyscripter.PumpState;
import de.jotomo.ruffyscripter.RuffyScripter;
/**
* Interface for all commands to be executed by the pump.
*
* Note on cammond methods and timing: a method shall wait before and after executing
* as necessary to not cause timing issues, so the caller can just call methods in
* sequence, letting the methods take care of waits.
*/
public interface Command {
CommandResult execute(RuffyScripter ruffyScripter, PumpState initialPumpState);
List<String> validateArguments();
}

View file

@ -0,0 +1,49 @@
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;
}
@Override
public String getMessage() {
return message;
}
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 + '\'' +
'}';
}
}

View file

@ -0,0 +1,66 @@
package de.jotomo.ruffyscripter.commands;
import java.util.Date;
import de.jotomo.ruffyscripter.History;
import de.jotomo.ruffyscripter.PumpState;
public class CommandResult {
public boolean success;
public boolean enacted;
public long completionTime;
public Exception exception;
public String message;
public PumpState state;
public History history;
public CommandResult() {
}
public CommandResult success(boolean success) {
this.success = success;
return this;
}
public CommandResult enacted(boolean enacted) {
this.enacted = enacted;
return this;
}
public CommandResult completionTime(long completionTime) {
this.completionTime = completionTime ;
return this;
}
public CommandResult exception(Exception exception) {
this.exception = exception;
return this;
}
public CommandResult message(String message) {
this.message = message;
return this;
}
public CommandResult state(PumpState state) {
this.state = state;
return this;
}
public CommandResult history(History history) {
this.history = history;
return this;
}
@Override
public String toString() {
return "CommandResult{" +
"success=" + success +
", enacted=" + enacted +
", completienTime=" + completionTime + "(" + new Date(completionTime) + ")" +
", exception=" + exception +
", message='" + message + '\'' +
", state=" + state +
'}';
}
}

View file

@ -0,0 +1,24 @@
package de.jotomo.ruffyscripter.commands;
import java.util.Collections;
import java.util.List;
import de.jotomo.ruffyscripter.PumpState;
import de.jotomo.ruffyscripter.RuffyScripter;
public class ReadPumpStateCommand implements Command {
@Override
public CommandResult execute(RuffyScripter ruffyScripter, PumpState initialPumpState) {
return new CommandResult().success(true).enacted(false).message("Returning pump state only");
}
@Override
public List<String> validateArguments() {
return Collections.emptyList();
}
@Override
public String toString() {
return "ReadPumpStateCommand{}";
}
}

View file

@ -0,0 +1,302 @@
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.ArrayList;
import java.util.List;
import java.util.Locale;
import de.jotomo.ruffyscripter.PumpState;
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;
}
@Override
public List<String> validateArguments() {
List<String> violations = new ArrayList<>();
if (percentage % 10 != 0) {
violations.add("TBR percentage must be set in 10% steps");
}
if (percentage < 0 || percentage > 500) {
violations.add("TBR percentage must be within 0-500%");
}
if (percentage != 100) {
if (duration % 15 != 0) {
violations.add("TBR duration can only be set in 15 minute steps");
}
if (duration > 60 * 24) {
violations.add("Maximum TBR duration is 24 hours");
}
}
if (percentage == 0 && duration > 120) {
violations.add("Max allowed zero-temp duration is 2h");
}
return violations;
}
@Override
public CommandResult execute(RuffyScripter scripter, PumpState initialPumpState) {
try {
enterTbrMenu(scripter);
inputTbrPercentage(scripter);
verifyDisplayedTbrPercentage(scripter);
if (percentage == 100) {
cancelTbrAndConfirmCancellationWarning(scripter);
} else {
// switch to TBR_DURATION menu by pressing menu key
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
scripter.pressMenuKey();
scripter.waitForMenuUpdate();
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
inputTbrDuration(scripter);
verifyDisplayedTbrDuration(scripter);
// confirm TBR
scripter.pressCheckKey();
scripter.waitForMenuToBeLeft(MenuType.TBR_DURATION);
}
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.verifyMenuIsDisplayed(MenuType.MAIN_MENU);
scripter.navigateToMenu(MenuType.TBR_MENU);
scripter.verifyMenuIsDisplayed(MenuType.TBR_MENU);
scripter.pressCheckKey();
scripter.waitForMenuUpdate();
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
}
private void inputTbrPercentage(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
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++) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
if (increasePercentage) scripter.pressUpKey();
else scripter.pressDownKey();
SystemClock.sleep(100);
log.debug("Push #" + (i + 1));
}
// Give the pump time to finish any scrolling that might still be going on, can take
// up to 1100ms. Plus some extra time to be sure
SystemClock.sleep(2000);
}
private void verifyDisplayedTbrPercentage(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
long displayedPercentage = readDisplayedTbrPercentage(scripter);
if (displayedPercentage != percentage) {
log.debug("Final displayed TBR percentage: " + displayedPercentage);
throw new CommandException().message("Failed to set TBR percentage");
}
// check again to ensure the displayed value hasn't change due to due scrolling taking extremely long
SystemClock.sleep(2000);
long refreshedDisplayedTbrPecentage = readDisplayedTbrPercentage(scripter);
if (displayedPercentage != refreshedDisplayedTbrPecentage) {
throw new CommandException().message("Failed to set TBR percentage: " +
"percentage changed after input stopped from "
+ displayedPercentage + " -> " + refreshedDisplayedTbrPecentage);
}
}
private long readDisplayedTbrPercentage(RuffyScripter scripter) {
// TODO v2 add timeout? Currently the command execution timeout would trigger if exceeded
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) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
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:13, hence not in 15 minute steps.
// Pressing up will go to the next higher 15 minute step.
// Don't press down, from 0:13 it can't go down, so press up.
// Pressing up from 23:59 works to go to 24:00.
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
scripter.pressUpKey();
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++) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
if (increaseDuration) scripter.pressUpKey();
else scripter.pressDownKey();
SystemClock.sleep(100);
log.debug("Push #" + (i + 1));
}
// Give the pump time to finish any scrolling that might still be going on, can take
// up to 1100ms. Plus some extra time to be sure
SystemClock.sleep(2000);
}
private void verifyDisplayedTbrDuration(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
long displayedDuration = readDisplayedTbrDuration(scripter);
if (displayedDuration != duration) {
log.debug("Final displayed TBR duration: " + displayedDuration);
throw new CommandException().message("Failed to set TBR duration");
}
// check again to ensure the displayed value hasn't change due to due scrolling taking extremely long
SystemClock.sleep(2000);
long refreshedDisplayedTbrDuration = readDisplayedTbrDuration(scripter);
if (displayedDuration != refreshedDisplayedTbrDuration) {
throw new CommandException().message("Failed to set TBR duration: " +
"duration changed after input stopped from "
+ displayedDuration + " -> " + refreshedDisplayedTbrDuration);
}
}
private long readDisplayedTbrDuration(RuffyScripter scripter) {
// TODO v2 add timeout? Currently the command execution timeout would trigger if exceeded
scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION);
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) {
// confirm entered TBR
scripter.verifyMenuIsDisplayed(MenuType.TBR_SET);
scripter.pressCheckKey();
// A "TBR CANCELLED alert" is only raised by the pump when the remaining time is
// greater than 60s (displayed as 0:01, the pump goes from there to finished.
// We could read the remaining duration from MAIN_MENU, but by the time we're here,
// the pumup could have moved from 0:02 to 0:01, so instead, check if a "TBR CANCELLED" alert
// is raised and if so dismiss it
long inFiveSeconds = System.currentTimeMillis() + 5 * 1000;
boolean alertProcessed = false;
while (System.currentTimeMillis() < inFiveSeconds && !alertProcessed) {
if (scripter.currentMenu.getType() == MenuType.WARNING_OR_ERROR) {
// Check the raised alarm is TBR CANCELLED, so we're not accidentally cancelling
// a different alarm that might be raised at the same time.
// Note that the message is permanently displayed, while the error code is blinking.
// A wait till the error code can be read results in the code hanging, despite
// menu updates coming in, so just check the message.
// TODO v2 this only works when the pump's language is English
String errorMsg = (String) scripter.currentMenu.getAttribute(MenuAttribute.MESSAGE);
if (!errorMsg.equals("TBR CANCELLED")) {
throw new CommandException().success(false).enacted(false)
.message("An alert other than the expected TBR CANCELLED was raised by the pump: "
+ errorMsg + ". Please check the pump.");
}
// confirm "TBR CANCELLED" alert
scripter.verifyMenuIsDisplayed(MenuType.WARNING_OR_ERROR);
scripter.pressCheckKey();
// dismiss "TBR CANCELLED" alert
scripter.verifyMenuIsDisplayed(MenuType.WARNING_OR_ERROR);
scripter.pressCheckKey();
scripter.waitForMenuToBeLeft(MenuType.WARNING_OR_ERROR);
alertProcessed = true;
}
SystemClock.sleep(10);
}
}
private void verifyMainMenuShowsNoActiveTbr(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) {
throw new CommandException().message("Cancelling TBR failed, TBR is still set according to MAIN_MENU");
}
}
private void verifyMainMenuShowsExpectedTbrActive(RuffyScripter scripter) {
scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU);
// new TBR set; percentage and duration must be displayed ...
if (!scripter.currentMenu.attributes().contains(MenuAttribute.TBR) ||
!scripter.currentMenu.attributes().contains(MenuAttribute.RUNTIME)) {
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");
}
}
@Override
public String toString() {
return "SetTbrCommand{" +
"percentage=" + percentage +
", duration=" + duration +
'}';
}
}

View file

@ -15,6 +15,7 @@ public class Config {
public static final boolean NSCLIENT = BuildConfig.NSCLIENTOLNY; public static final boolean NSCLIENT = BuildConfig.NSCLIENTOLNY;
public static final boolean COMBO = true && BuildConfig.PUMPDRIVERS;
public static final boolean DANAR = true && BuildConfig.PUMPDRIVERS; public static final boolean DANAR = true && BuildConfig.PUMPDRIVERS;
public static final boolean DANARv2 = true && BuildConfig.PUMPDRIVERS; public static final boolean DANARv2 = true && BuildConfig.PUMPDRIVERS;

View file

@ -44,6 +44,7 @@ import info.nightscout.androidaps.plugins.ProfileCircadianPercentage.CircadianPe
import info.nightscout.androidaps.plugins.ProfileLocal.LocalProfileFragment; import info.nightscout.androidaps.plugins.ProfileLocal.LocalProfileFragment;
import info.nightscout.androidaps.plugins.ProfileNS.NSProfileFragment; import info.nightscout.androidaps.plugins.ProfileNS.NSProfileFragment;
import info.nightscout.androidaps.plugins.ProfileSimple.SimpleProfileFragment; import info.nightscout.androidaps.plugins.ProfileSimple.SimpleProfileFragment;
import info.nightscout.androidaps.plugins.PumpCombo.ComboFragment;
import info.nightscout.androidaps.plugins.PumpDanaR.DanaRFragment; import info.nightscout.androidaps.plugins.PumpDanaR.DanaRFragment;
import info.nightscout.androidaps.plugins.PumpDanaR.services.DanaRExecutionService; import info.nightscout.androidaps.plugins.PumpDanaR.services.DanaRExecutionService;
import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanFragment; import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanFragment;
@ -119,6 +120,7 @@ public class MainApp extends Application {
if (Config.DANAR) pluginsList.add(DanaRFragment.getPlugin()); if (Config.DANAR) pluginsList.add(DanaRFragment.getPlugin());
if (Config.DANAR) pluginsList.add(DanaRKoreanFragment.getPlugin()); if (Config.DANAR) pluginsList.add(DanaRKoreanFragment.getPlugin());
if (Config.DANARv2) pluginsList.add(DanaRv2Fragment.getPlugin()); if (Config.DANARv2) pluginsList.add(DanaRv2Fragment.getPlugin());
if (Config.COMBO) pluginsList.add(ComboFragment.getPlugin());
pluginsList.add(CareportalFragment.getPlugin()); pluginsList.add(CareportalFragment.getPlugin());
if (Config.MDI) pluginsList.add(MDIPlugin.getPlugin()); if (Config.MDI) pluginsList.add(MDIPlugin.getPlugin());
if (Config.VIRTUALPUMP) pluginsList.add(VirtualPumpPlugin.getInstance()); if (Config.VIRTUALPUMP) pluginsList.add(VirtualPumpPlugin.getInstance());

View file

@ -14,6 +14,7 @@ import android.preference.PreferenceManager;
import info.nightscout.androidaps.events.EventPreferenceChange; import info.nightscout.androidaps.events.EventPreferenceChange;
import info.nightscout.androidaps.events.EventRefreshGui; import info.nightscout.androidaps.events.EventRefreshGui;
import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.interfaces.PluginBase;
import info.nightscout.androidaps.plugins.PumpCombo.ComboPlugin;
import info.nightscout.androidaps.plugins.PumpDanaR.BluetoothDevicePreference; import info.nightscout.androidaps.plugins.PumpDanaR.BluetoothDevicePreference;
import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin;
import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanPlugin; import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanPlugin;
@ -135,6 +136,12 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre
addPreferencesFromResource(R.xml.pref_danarprofile); addPreferencesFromResource(R.xml.pref_danarprofile);
} }
} }
if (Config.COMBO) {
ComboPlugin comboPlugin = (ComboPlugin) MainApp.getSpecificPlugin(ComboPlugin.class);
if (comboPlugin.isEnabled(PluginBase.PUMP)) {
addPreferencesFromResource(R.xml.pref_combo);
}
}
VirtualPumpPlugin virtualPumpPlugin = (VirtualPumpPlugin) MainApp.getSpecificPlugin(VirtualPumpPlugin.class); VirtualPumpPlugin virtualPumpPlugin = (VirtualPumpPlugin) MainApp.getSpecificPlugin(VirtualPumpPlugin.class);
if (virtualPumpPlugin != null && virtualPumpPlugin.isEnabled(PluginBase.PUMP)) { if (virtualPumpPlugin != null && virtualPumpPlugin.isEnabled(PluginBase.PUMP)) {
addPreferencesFromResource(R.xml.pref_virtualpump); addPreferencesFromResource(R.xml.pref_virtualpump);

View file

@ -252,6 +252,11 @@ public class TemporaryBasal implements Interval {
return Math.round(msecs / 60f / 1000); return Math.round(msecs / 60f / 1000);
} }
public int getPlannedRemainingSeconds() {
Float remainingMin = (end() - System.currentTimeMillis()) / 1000f;
return remainingMin.intValue();
}
public int getPlannedRemainingMinutes() { public int getPlannedRemainingMinutes() {
float remainingMin = (end() - System.currentTimeMillis()) / 1000f / 60; float remainingMin = (end() - System.currentTimeMillis()) / 1000f / 60;
return (remainingMin < 0) ? 0 : Math.round(remainingMin); return (remainingMin < 0) ? 0 : Math.round(remainingMin);
@ -280,6 +285,8 @@ public class TemporaryBasal implements Interval {
", isAbsolute=" + isAbsolute + ", isAbsolute=" + isAbsolute +
", isFakeExtended=" + isFakeExtended + ", isFakeExtended=" + isFakeExtended +
", netExtendedRate=" + netExtendedRate + ", netExtendedRate=" + netExtendedRate +
", minutesRemaining=" + getPlannedRemainingMinutes() +
", secondsRemaining=" + getPlannedRemainingSeconds() +
'}'; '}';
} }

View file

@ -578,7 +578,10 @@ public class ConfigBuilderPlugin implements PluginBase, PumpInterface, Constrain
result.comment = "Temp basal set correctly"; result.comment = "Temp basal set correctly";
result.success = true; result.success = true;
if (Config.logCongigBuilderActions) if (Config.logCongigBuilderActions)
log.debug("applyAPSRequest: Temp basal set correctly"); log.debug("applyAPSRequest: Temp basal set correctly "
+ "(no pump request needed, pump is still running requested rate of "
+ request.rate + " for "
+ (int) getTempBasalRemainingMinutesFromHistory() + " more minutes)");
} else { } else {
if (Config.logCongigBuilderActions) if (Config.logCongigBuilderActions)
log.debug("applyAPSRequest: setTempBasalAbsolute()"); log.debug("applyAPSRequest: setTempBasalAbsolute()");

View file

@ -0,0 +1,145 @@
package info.nightscout.androidaps.plugins.PumpCombo;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.jotomo.ruffyscripter.PumpState;
import de.jotomo.ruffyscripter.commands.Command;
import de.jotomo.ruffyscripter.commands.CommandResult;
import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.R;
import info.nightscout.androidaps.plugins.PumpCombo.events.EventComboPumpUpdateGUI;
public class ComboFragment extends Fragment implements View.OnClickListener {
private static Logger log = LoggerFactory.getLogger(ComboFragment.class);
private static ComboPlugin comboPlugin = new ComboPlugin();
public static ComboPlugin getPlugin() {
return comboPlugin;
}
private Button refresh;
private TextView statusText;
private TextView tbrPercentageText;
private TextView tbrDurationText;
private TextView tbrRateText;
private TextView pumpErrorText;
private TextView lastCmdText;
private TextView lastCmdTimeText;
private TextView lastCmdResultText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.combopump_fragment, container, false);
refresh = (Button) view.findViewById(R.id.combo_refresh);
statusText = (TextView) view.findViewById(R.id.combo_status);
tbrPercentageText = (TextView) view.findViewById(R.id.combo_tbr_percentage);
tbrDurationText = (TextView) view.findViewById(R.id.combo_tbr_duration);
tbrRateText = (TextView) view.findViewById(R.id.combo_tbr_rate);
pumpErrorText = (TextView) view.findViewById(R.id.combo_pump_error);
lastCmdText = (TextView) view.findViewById(R.id.combo_last_command);
lastCmdTimeText = (TextView) view.findViewById(R.id.combo_last_command_time);
lastCmdResultText = (TextView) view.findViewById(R.id.combo_last_command_result);
refresh.setOnClickListener(this);
updateGUI();
return view;
}
@Override
public void onPause() {
super.onPause();
MainApp.bus().unregister(this);
}
@Override
public void onResume() {
super.onResume();
MainApp.bus().register(this);
updateGUI();
}
@Subscribe
public void onStatusEvent(final EventComboPumpUpdateGUI ev) {
updateGUI();
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.combo_refresh:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
getPlugin().refreshDataFromPump("User request");
}
});
thread.start();
break;
}
}
public void updateGUI() {
Activity activity = getActivity();
if (activity != null)
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
statusText.setText(getPlugin().statusSummary);
if (getPlugin().isInitialized()) {
PumpState ps = getPlugin().pumpState;
if (ps != null) {
boolean tbrActive = ps.tbrPercent != -1 && ps.tbrPercent != 100;
if (tbrActive) {
tbrPercentageText.setText("" + ps.tbrPercent + "%");
tbrDurationText.setText("" + ps.tbrRemainingDuration + " min");
tbrRateText.setText("" + ps.tbrRate + " U/h");
} else {
tbrPercentageText.setText("Default basal rate running");
tbrDurationText.setText("");
tbrRateText.setText("");
}
pumpErrorText.setText(ps.errorMsg != null ? ps.errorMsg : "");
}
Command lastCmd = getPlugin().lastCmd;
if (lastCmd != null) {
lastCmdText.setText(lastCmd.toString());
lastCmdTimeText.setText(getPlugin().lastCmdTime.toLocaleString());
} else {
lastCmdText.setText("");
lastCmdTimeText.setText("");
}
CommandResult lastCmdResult = getPlugin().lastCmdResult;
if (lastCmdResult != null && lastCmdResult.message != null) {
lastCmdResultText.setText(lastCmdResult.message);
} else {
lastCmdResultText.setText("");
}
}
}
});
}
}

View file

@ -0,0 +1,622 @@
package info.nightscout.androidaps.plugins.PumpCombo;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import com.squareup.otto.Subscribe;
import org.json.JSONException;
import org.json.JSONObject;
import org.monkey.d.ruffy.ruffy.driver.IRuffyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import de.jotomo.ruffyscripter.RuffyScripter;
import de.jotomo.ruffyscripter.commands.BolusCommand;
import de.jotomo.ruffyscripter.commands.CancelTbrCommand;
import de.jotomo.ruffyscripter.commands.Command;
import de.jotomo.ruffyscripter.commands.CommandResult;
import de.jotomo.ruffyscripter.commands.ReadPumpStateCommand;
import de.jotomo.ruffyscripter.commands.SetTbrCommand;
import de.jotomo.ruffyscripter.PumpState;
import info.nightscout.androidaps.BuildConfig;
import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.R;
import info.nightscout.androidaps.data.DetailedBolusInfo;
import info.nightscout.androidaps.data.Profile;
import info.nightscout.androidaps.data.PumpEnactResult;
import info.nightscout.androidaps.db.Source;
import info.nightscout.androidaps.db.TemporaryBasal;
import info.nightscout.androidaps.events.EventAppExit;
import info.nightscout.androidaps.interfaces.PluginBase;
import info.nightscout.androidaps.interfaces.PumpDescription;
import info.nightscout.androidaps.interfaces.PumpInterface;
import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin;
import info.nightscout.androidaps.plugins.PumpCombo.events.EventComboPumpUpdateGUI;
import info.nightscout.utils.DateUtil;
/**
* Created by mike on 05.08.2016.
*/
public class ComboPlugin implements PluginBase, PumpInterface {
private static Logger log = LoggerFactory.getLogger(ComboPlugin.class);
private boolean fragmentEnabled = false;
private boolean fragmentVisible = false;
private PumpDescription pumpDescription = new PumpDescription();
private RuffyScripter ruffyScripter;
private ServiceConnection mRuffyServiceConnection;
// package-protected only so ComboFragment can access these
@NonNull
volatile String statusSummary = "Initializing";
@Nullable
volatile Command lastCmd;
@Nullable
volatile CommandResult lastCmdResult;
@NonNull
volatile Date lastCmdTime = new Date(0);
volatile PumpState pumpState = new PumpState();
private static PumpEnactResult OPERATION_NOT_SUPPORTED = new PumpEnactResult();
static {
OPERATION_NOT_SUPPORTED.success = false;
OPERATION_NOT_SUPPORTED.enacted = false;
OPERATION_NOT_SUPPORTED.comment = "Requested operation not supported by pump";
}
public ComboPlugin() {
definePumpCapabilities();
MainApp.bus().register(this);
bindRuffyService();
startAlerter();
}
private void definePumpCapabilities() {
pumpDescription.isBolusCapable = true;
pumpDescription.bolusStep = 0.1d;
pumpDescription.isExtendedBolusCapable = false; // TODO
pumpDescription.extendedBolusStep = 0.1d;
pumpDescription.extendedBolusDurationStep = 15;
pumpDescription.extendedBolusMaxDuration = 12 * 60;
pumpDescription.isTempBasalCapable = true;
pumpDescription.tempBasalStyle = PumpDescription.PERCENT;
pumpDescription.maxTempPercent = 500;
pumpDescription.tempPercentStep = 10;
pumpDescription.tempDurationStep = 15;
pumpDescription.tempMaxDuration = 24 * 60;
pumpDescription.isSetBasalProfileCapable = false; // TODO
pumpDescription.basalStep = 0.01d;
pumpDescription.basalMinimumRate = 0.0d;
pumpDescription.isRefillingCapable = true;
}
/**
* The alerter frequently checks the result of the last executed command via the lastCmdResult
* field and shows a notification with sound and vibration if an error occurred.
* More details on the error can then be looked up in the Combo tab.
*
* The alarm is re-raised every 5 minutes for as long as the error persist. As soon
* as a command succeeds no more new alerts are raised.
*/
private void startAlerter() {
new Thread(new Runnable() {
@Override
public void run() {
Context context = MainApp.instance().getApplicationContext();
NotificationManager mgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int id = 1000;
long lastAlarmTime = 0;
while (true) {
Command localLastCmd = lastCmd;
CommandResult localLastCmdResult = lastCmdResult;
if (localLastCmdResult != null && !localLastCmdResult.success) {
long now = System.currentTimeMillis();
long fiveMinutesSinceLastAlarm = lastAlarmTime + (5 * 60 * 1000) + (15 * 1000);
if (now > fiveMinutesSinceLastAlarm) {
log.error("Command failed: " + localLastCmd);
log.error("Command result: " + localLastCmdResult);
PumpState localPumpState = pumpState;
if (localPumpState != null && localPumpState.errorMsg != null) {
log.warn("Pump is in error state, displaying; " + localPumpState.errorMsg);
}
long[] vibratePattern = new long[]{1000, 2000, 1000, 2000, 1000};
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.notif_icon)
.setSmallIcon(R.drawable.icon_bolus)
.setContentTitle("Combo communication error")
.setContentText(localLastCmdResult.message)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setLights(Color.BLUE, 1000, 0)
.setSound(uri)
.setVibrate(vibratePattern);
mgr.notify(id, notificationBuilder.build());
lastAlarmTime = now;
} else {
log.warn("Pump still in error state, but alarm raised recently, so not triggering again: " + localLastCmdResult.message);
}
}
SystemClock.sleep(5 * 1000);
}
}
}, "combo-alerter").start();
}
private void bindRuffyService() {
Context context = MainApp.instance().getApplicationContext();
boolean boundSucceeded = false;
try {
Intent intent = new Intent()
.setComponent(new ComponentName(
// this must be the base package of the app (check package attribute in
// manifest element in the manifest file of the providing app)
"org.monkey.d.ruffy.ruffy",
// full path to the driver
// in the logs this service is mentioned as (note the slash)
// "org.monkey.d.ruffy.ruffy/.driver.Ruffy"
"org.monkey.d.ruffy.ruffy.driver.Ruffy"
));
context.startService(intent);
mRuffyServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ruffyScripter = new RuffyScripter(IRuffyService.Stub.asInterface(service));
log.debug("ruffy serivce connected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
log.debug("ruffy service disconnected");
}
};
boundSucceeded = context.bindService(intent, mRuffyServiceConnection, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
log.error("Binding to ruffy service failed", e);
}
if (!boundSucceeded) {
statusSummary = "No connection to ruffy. Pump control not available.";
}
}
private void unbindRuffyService() {
MainApp.instance().getApplicationContext().unbindService(mRuffyServiceConnection);
}
@Override
public String getFragmentClass() {
return ComboFragment.class.getName();
}
@Override
public String getName() {
return MainApp.instance().getString(R.string.combopump);
}
@Override
public String getNameShort() {
String name = MainApp.sResources.getString(R.string.combopump_shortname);
if (!name.trim().isEmpty()) {
//only if translation exists
return name;
}
// use long name as fallback
return getName();
}
@Override
public boolean isEnabled(int type) {
return type == PUMP && fragmentEnabled;
}
@Override
public boolean isVisibleInTabs(int type) {
return type == PUMP && fragmentVisible;
}
@Override
public boolean canBeHidden(int type) {
return true;
}
@Override
public boolean hasFragment() {
return true;
}
@Override
public boolean showInList(int type) {
return true;
}
@Override
public void setFragmentEnabled(int type, boolean fragmentEnabled) {
if (type == PUMP) this.fragmentEnabled = fragmentEnabled;
}
@Override
public void setFragmentVisible(int type, boolean fragmentVisible) {
if (type == PUMP) this.fragmentVisible = fragmentVisible;
}
@Override
public int getType() {
return PluginBase.PUMP;
}
@Override
public boolean isInitialized() {
// consider initialized when the pump's state was initially fetched,
// after that lastCmd* variables will have values
return lastCmdTime.getTime() > 0;
}
@Override
public boolean isSuspended() {
return pumpState != null && pumpState.suspended;
}
@Override
public boolean isBusy() {
return ruffyScripter == null || ruffyScripter.isPumpBusy();
}
// TODO
@Override
public int setNewBasalProfile(Profile profile) {
return FAILED;
}
// TODO
@Override
public boolean isThisProfileSet(Profile profile) {
return false;
}
@Override
public Date lastDataTime() {
return lastCmdTime;
}
// this method is regularly called from info.nightscout.androidaps.receivers.KeepAliveReceiver
@Override
public void refreshDataFromPump(String reason) {
log.debug("RefreshDataFromPump called");
// if Android is sluggish this might get called before ruffy is bound
if (ruffyScripter == null) {
log.warn("Rejecting call to RefreshDataFromPump: ruffy service not bound (yet)");
return;
}
if (!reason.toLowerCase().contains("user")
&& lastCmdTime.getTime() > 0
&& System.currentTimeMillis() > lastCmdTime.getTime() + 60 * 1000) {
log.debug("Not fetching state from pump, since we did already within the last 60 seconds");
} else {
runCommand(new ReadPumpStateCommand());
}
}
// TODO uses profile values for the time being
// this get's called mulitple times a minute, must absolutely be cached
@Override
public double getBaseBasalRate() {
Profile profile = MainApp.getConfigBuilder().getProfile();
Double basal = profile.getBasal();
log.trace("getBaseBasalrate returning " + basal);
return basal;
}
// what a mess: pump integration code reading carb info from Detailed**Bolus**Info,
// writing carb treatments to the history table. What's PumpEnactResult for again?
@Override
public PumpEnactResult deliverTreatment(DetailedBolusInfo detailedBolusInfo) {
if (detailedBolusInfo.insulin > 0 || detailedBolusInfo.carbs > 0) {
if (detailedBolusInfo.insulin > 0) {
// bolus needed, ask pump to deliver it
CommandResult bolusCmdResult = runCommand(new BolusCommand(detailedBolusInfo.insulin));
PumpEnactResult pumpEnactResult = new PumpEnactResult();
pumpEnactResult.success = bolusCmdResult.success;
pumpEnactResult.enacted = bolusCmdResult.enacted;
pumpEnactResult.comment = bolusCmdResult.message;
// if enacted, add bolus and carbs to treatment history
if (pumpEnactResult.enacted) {
// TODO if no error occurred, the requested bolus is what the pump delievered,
// that has been checked. If an error occurred, we should check how much insulin
// was delivered, e.g. when the cartridge went empty mid-bolus
// For the first iteration, the alert the pump raises must suffice
pumpEnactResult.bolusDelivered = detailedBolusInfo.insulin;
pumpEnactResult.carbsDelivered = detailedBolusInfo.carbs;
detailedBolusInfo.date = bolusCmdResult.completionTime;
MainApp.getConfigBuilder().addToHistoryTreatment(detailedBolusInfo);
} else {
pumpEnactResult.bolusDelivered = 0d;
pumpEnactResult.carbsDelivered = 0d;
}
return pumpEnactResult;
} else {
// no bolus required, carb only treatment
// TODO the ui freezes when the calculator issues a carb-only treatment
// so just wait, yeah, this is dumb. for now; proper fix via GL#10
// info.nightscout.androidaps.plugins.Overview.Dialogs.BolusProgressDialog.scheduleDismiss()
SystemClock.sleep(6000);
PumpEnactResult pumpEnactResult = new PumpEnactResult();
pumpEnactResult.success = true;
pumpEnactResult.enacted = true;
pumpEnactResult.bolusDelivered = 0d;
pumpEnactResult.carbsDelivered = detailedBolusInfo.carbs;
pumpEnactResult.comment = MainApp.instance().getString(R.string.virtualpump_resultok);
MainApp.getConfigBuilder().addToHistoryTreatment(detailedBolusInfo);
return pumpEnactResult;
}
} else {
// neither carbs nor bolus requested
PumpEnactResult pumpEnactResult = new PumpEnactResult();
pumpEnactResult.success = false;
pumpEnactResult.enacted = false;
pumpEnactResult.bolusDelivered = 0d;
pumpEnactResult.carbsDelivered = 0d;
pumpEnactResult.comment = MainApp.instance().getString(R.string.danar_invalidinput);
log.error("deliverTreatment: Invalid input");
return pumpEnactResult;
}
}
private CommandResult runCommand(Command command) {
if (ruffyScripter == null) {
String msg = "No connection to ruffy. Pump control not available.";
statusSummary = msg;
return new CommandResult().message(msg);
}
statusSummary = "Executing " + command;
MainApp.bus().post(new EventComboPumpUpdateGUI());
CommandResult commandResult = ruffyScripter.runCommand(command);
if (!commandResult.success && commandResult.exception != null) {
log.error("CommandResult has exception, rebinding ruffy service", commandResult.exception);
// attempt to rebind the ruffy service, will start ruffy again if it crashed
try {
unbindRuffyService();
SystemClock.sleep(5000);
bindRuffyService();
SystemClock.sleep(5000);
} catch (Exception e) {
String msg = "No connection to ruffy. Pump control not available.";
statusSummary = msg;
return new CommandResult().message(msg);
}
if (ruffyScripter == null) {
log.error("Rebinding failed");
} else if (!commandResult.enacted && !(command instanceof BolusCommand)) {
// retry command, but make sure it wasn't enacted and don't retry
// bolus commands (user is interacting with AAPS right now so he can
// deal with it and we don't want to deliver a bolus twice)
CommandResult retriedCommandResult = ruffyScripter.runCommand(command);
if (retriedCommandResult.success) {
commandResult = retriedCommandResult;
}
}
}
log.debug("RuffyScripter returned from command invocation, result: " + commandResult);
if (commandResult.exception != null) {
log.error("Exception received from pump", commandResult.exception);
}
lastCmd = command;
lastCmdTime = new Date();
lastCmdResult = commandResult;
pumpState = commandResult.state;
if (commandResult.success && commandResult.state.suspended) {
statusSummary = "Suspended";
} else if (commandResult.success) {
statusSummary = "Idle";
} else {
statusSummary = "Error";
}
MainApp.bus().post(new EventComboPumpUpdateGUI());
return commandResult;
}
@Override
public void stopBolusDelivering() {
// there's no way to stop the combo once delivery has started
// but before that, we could interrupt the command thread ... pause
// till pump times out or raises an error
}
// Note: AAPS calls this only to enact OpenAPS recommendations
@Override
public PumpEnactResult setTempBasalAbsolute(Double absoluteRate, Integer durationInMinutes) {
log.debug("setTempBasalAbsolute called with a rate of " + absoluteRate + " for " + durationInMinutes + " min.");
int unroundedPercentage = Double.valueOf(absoluteRate / getBaseBasalRate() * 100).intValue();
int roundedPercentage = (int) (Math.round(absoluteRate / getBaseBasalRate() * 10) * 10);
if (unroundedPercentage != roundedPercentage) {
log.debug("Rounded requested rate " + unroundedPercentage + "% -> " + roundedPercentage + "%");
}
return setTempBasalPercent(roundedPercentage, durationInMinutes);
}
// Note: AAPS calls this only for setting a temp basal issued by the user
@Override
public PumpEnactResult setTempBasalPercent(Integer percent, Integer durationInMinutes) {
log.debug("setTempBasalPercent called with " + percent + "% for " + durationInMinutes + "min");
if (percent % 10 != 0) {
int rounded = percent;
while (rounded % 10 != 0) rounded = rounded - 1;
log.debug("Rounded requested percentage from " + percent + " to " + rounded);
percent = rounded;
}
CommandResult commandResult = runCommand(new SetTbrCommand(percent, durationInMinutes));
if (commandResult.enacted) {
TemporaryBasal tempStart = new TemporaryBasal(commandResult.completionTime);
// TODO commandResult.state.tbrRemainingDuration might already display 29 if 30 was set, since 29:59 is shown as 29 ...
// we should check this, but really ... something must be really screwed up if that number was anything different
tempStart.durationInMinutes = durationInMinutes;
tempStart.percentRate = percent;
tempStart.isAbsolute = false;
tempStart.source = Source.USER;
ConfigBuilderPlugin treatmentsInterface = MainApp.getConfigBuilder();
treatmentsInterface.addToHistoryTempBasal(tempStart);
}
PumpEnactResult pumpEnactResult = new PumpEnactResult();
pumpEnactResult.success = commandResult.success;
pumpEnactResult.enacted = commandResult.enacted;
pumpEnactResult.comment = commandResult.message;
pumpEnactResult.isPercent = true;
// Combo would have bailed if this wasn't set properly. Maybe we should
// have the command return this anyways ...
pumpEnactResult.percent = percent;
pumpEnactResult.duration = durationInMinutes;
return pumpEnactResult;
}
@Override
public PumpEnactResult setExtendedBolus(Double insulin, Integer durationInMinutes) {
return OPERATION_NOT_SUPPORTED;
}
@Override
public PumpEnactResult cancelTempBasal() {
log.debug("cancelTempBasal called");
CommandResult commandResult = runCommand(new CancelTbrCommand());
if (commandResult.enacted) {
TemporaryBasal tempStop = new TemporaryBasal(commandResult.completionTime);
tempStop.durationInMinutes = 0; // ending temp basal
tempStop.source = Source.USER;
ConfigBuilderPlugin treatmentsInterface = MainApp.getConfigBuilder();
treatmentsInterface.addToHistoryTempBasal(tempStop);
}
PumpEnactResult pumpEnactResult = new PumpEnactResult();
pumpEnactResult.success = commandResult.success;
pumpEnactResult.enacted = commandResult.enacted;
pumpEnactResult.comment = commandResult.message;
pumpEnactResult.isTempCancel = true;
return pumpEnactResult;
}
// TODO
@Override
public PumpEnactResult cancelExtendedBolus() {
return OPERATION_NOT_SUPPORTED;
}
// Returns the state of the pump as it was received during last pump comms.
// TODO v2 add battery, reservoir info when we start reading that and clean up the code
@Override
public JSONObject getJSONStatus() {
if (lastCmdTime.getTime() + 5 * 60 * 1000L < System.currentTimeMillis()) {
return null;
}
try {
JSONObject pump = new JSONObject();
JSONObject status = new JSONObject();
JSONObject extended = new JSONObject();
status.put("status", statusSummary);
extended.put("Version", BuildConfig.VERSION_NAME + "-" + BuildConfig.BUILDVERSION);
try {
extended.put("ActiveProfile", MainApp.getConfigBuilder().getProfileName());
} catch (Exception e) {
}
status.put("timestamp", lastCmdTime);
PumpState ps = pumpState;
if (ps != null) {
if (ps.tbrActive) {
extended.put("TempBasalAbsoluteRate", ps.tbrRate);
extended.put("TempBasalPercent", ps.tbrPercent);
extended.put("TempBasalRemaining", ps.tbrRemainingDuration);
}
if (ps.errorMsg != null) {
extended.put("ErrorMessage", ps.errorMsg);
}
}
// more info here .... look at dana plugin
pump.put("status", status);
pump.put("extended", extended);
pump.put("clock", DateUtil.toISOString(lastCmdTime));
return pump;
} catch (Exception e) {
log.warn("Failed to gather device status for upload", e);
}
return null;
}
// TODO
@Override
public String deviceID() {
// Serial number here
return "Combo";
}
@Override
public PumpDescription getPumpDescription() {
return pumpDescription;
}
@Override
public String shortStatus(boolean veryShort) {
return statusSummary;
}
@Override
public boolean isFakingTempsByExtendedBoluses() {
return false;
}
@SuppressWarnings("UnusedParameters")
@Subscribe
public void onStatusEvent(final EventAppExit e) {
unbindRuffyService();
}
}
// If you want update fragment call
// MainApp.bus().post(new EventComboPumpUpdateGUI());
// fragment should fetch data from plugin and display status, buttons etc ...

View file

@ -0,0 +1,8 @@
package info.nightscout.androidaps.plugins.PumpCombo.events;
/**
* Created by mike on 24.05.2017.
*/
public class EventComboPumpUpdateGUI {
}

View file

@ -263,7 +263,9 @@ public class TreatmentsPlugin implements PluginBase, TreatmentsInterface {
@Override @Override
public boolean isTempBasalInProgress() { public boolean isTempBasalInProgress() {
return getTempBasalFromHistory(System.currentTimeMillis()) != null; TemporaryBasal tempBasalFromHistory = getTempBasalFromHistory(System.currentTimeMillis());
log.debug("activeTempbasal: " + tempBasalFromHistory);
return tempBasalFromHistory != null && tempBasalFromHistory.getPlannedRemainingSeconds() > 60;
} }
@Override @Override

View file

@ -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<MenuAttribute,Object> 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<MenuAttribute> attributes()
{
return new LinkedList<MenuAttribute>(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<Menu> CREATOR = new
Creator<Menu>() {
public Menu createFromParcel(Parcel in) {
return new Menu(in);
}
public Menu[] newArray(int size) {
return new Menu[size];
}
};
}

View file

@ -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
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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";
}
}

View file

@ -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)+".";
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,388 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".plugins.PumpCombo.ComboFragment">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/combo_refresh"
style="?android:attr/buttonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Refresh" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="Status"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="TBR percentage"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_tbr_percentage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="TBR duration"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_tbr_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="TBR rate"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_tbr_rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="Pump error"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_pump_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="Last command"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_last_command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="Command time"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_last_command_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingRight="5dp"
android:text="Command result"
android:textSize="14sp" />
<TextView
android:layout_width="5dp"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center_horizontal"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text=":"
android:textSize="14sp" />
<TextView
android:id="@+id/combo_last_command_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingLeft="5dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@color/listdelimiter" />
</LinearLayout>
</ScrollView>
</FrameLayout>

View file

@ -687,6 +687,9 @@
<string name="cpp_valuesnotstored">Values not stored!</string> <string name="cpp_valuesnotstored">Values not stored!</string>
<string name="wear_overviewnotifications">Overview Notifications</string> <string name="wear_overviewnotifications">Overview Notifications</string>
<string name="wear_overviewnotifications_summary">Pass the Overview Notifications through as wear confirmation messages.</string> <string name="wear_overviewnotifications_summary">Pass the Overview Notifications through as wear confirmation messages.</string>
<string name="combopump">Accu-Chek Combo</string>
<string name="combopump_settings">Accu-Chek Combo settings</string>
<string name="combopump_shortname">COMBO</string>
<string name="ns_localbroadcasts">Enable broadcasts to other apps (like xDrip).</string> <string name="ns_localbroadcasts">Enable broadcasts to other apps (like xDrip).</string>
<string name="ns_localbroadcasts_title">Enable local Broadcasts.</string> <string name="ns_localbroadcasts_title">Enable local Broadcasts.</string>
<string name="careportal_activity_label">ACTIVITY &amp; FEEDBACK</string> <string name="careportal_activity_label">ACTIVITY &amp; FEEDBACK</string>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="combopump"
android:title="@string/combopump_settings">
</PreferenceCategory>
</PreferenceScreen>