Merge branch 'stable' into '1_5f'
# Conflicts: # app/src/main/res/values/strings.xml
This commit is contained in:
commit
bfd96b0635
|
@ -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'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.monkey.d.ruffy.ruffy.driver.display;
|
||||||
|
|
||||||
|
parcelable Menu;
|
|
@ -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>
|
||||||
|
|
||||||
|
|
5
app/src/main/java/de/jotomo/ruffyscripter/History.java
Normal file
5
app/src/main/java/de/jotomo/ruffyscripter/History.java
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package de.jotomo.ruffyscripter;
|
||||||
|
|
||||||
|
/** The history data read from "My data" */
|
||||||
|
public class History {
|
||||||
|
}
|
65
app/src/main/java/de/jotomo/ruffyscripter/PumpState.java
Normal file
65
app/src/main/java/de/jotomo/ruffyscripter/PumpState.java
Normal 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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
481
app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java
Normal file
481
app/src/main/java/de/jotomo/ruffyscripter/RuffyScripter.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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{}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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{}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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() +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()");
|
||||||
|
|
|
@ -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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ...
|
|
@ -0,0 +1,8 @@
|
||||||
|
package info.nightscout.androidaps.plugins.PumpCombo.events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by mike on 24.05.2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class EventComboPumpUpdateGUI {
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)+".";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
388
app/src/main/res/layout/combopump_fragment.xml
Normal file
388
app/src/main/res/layout/combopump_fragment.xml
Normal 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>
|
|
@ -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 & FEEDBACK</string>
|
<string name="careportal_activity_label">ACTIVITY & FEEDBACK</string>
|
||||||
|
|
9
app/src/main/res/xml/pref_combo.xml
Normal file
9
app/src/main/res/xml/pref_combo.xml
Normal 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>
|
Loading…
Reference in a new issue