Skip to content
Snippets Groups Projects
Commit cd47fcc8 authored by Lennart Goedhart's avatar Lennart Goedhart
Browse files

Merge branch 'feature/PR104' of https://github.com/pazaan/640gAndroidUploader...

Merge branch 'feature/PR104' of https://github.com/pazaan/640gAndroidUploader into pazaan/develop-basal-rates
parents 06139e97 2ad7fb33
Branches
Tags
No related merge requests found
Showing
with 263 additions and 58 deletions
...@@ -145,7 +145,7 @@ release { ...@@ -145,7 +145,7 @@ release {
dependencies { dependencies {
compile files('libs/slf4j-api-1.7.2.jar') compile files('libs/slf4j-api-1.7.2.jar')
compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { compile('com.crashlytics.sdk.android:crashlytics:2.6.6@aar') {
transitive = true; transitive = true;
} }
compile('com.mikepenz:materialdrawer:5.2.9@aar') { compile('com.mikepenz:materialdrawer:5.2.9@aar') {
......
...@@ -31,11 +31,13 @@ ...@@ -31,11 +31,13 @@
<!-- I have set screenOrientation to "portrait" to avoid the restart of AsyncTasks when you rotate the phone --> <!-- I have set screenOrientation to "portrait" to avoid the restart of AsyncTasks when you rotate the phone -->
<!-- configChanges="uiMode" added to avoid restart of AsyncTasks when phone is plugged into charger/dock (for phones that can do usb otg & charging simultaneously -->
<activity <activity
android:name=".medtronic.MainActivity" android:name=".medtronic.MainActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="uiMode"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
<intent-filter android:icon="@drawable/ic_launcher"> <intent-filter android:icon="@drawable/ic_launcher">
......
...@@ -58,6 +58,7 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; ...@@ -58,6 +58,7 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Queue; import java.util.Queue;
...@@ -94,8 +95,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -94,8 +95,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
private boolean hasZoomedChart = false; private boolean hasZoomedChart = false;
private NumberFormat sgvFormatter; private NumberFormat sgvFormatter;
private boolean mmolxl; private static boolean mmolxl;
private boolean mmolxlDecimals; private static boolean mmolxlDecimals;
public static long timeLastGoodSGV = 0;
public static short pumpBattery = 0;
public static int countUnavailableSGV = 0;
boolean mEnableCgmService = true; boolean mEnableCgmService = true;
SharedPreferences prefs = null; SharedPreferences prefs = null;
...@@ -139,6 +144,20 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -139,6 +144,20 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
return nextPoll; return nextPoll;
} }
public static String strFormatSGV(float sgvValue) {
if (mmolxl) {
NumberFormat sgvFormatter;
if (mmolxlDecimals) {
sgvFormatter = new DecimalFormat("0.00");
} else {
sgvFormatter = new DecimalFormat("0.0");
}
return sgvFormatter.format(sgvValue / MMOLXLFACTOR);
} else {
return String.valueOf(sgvValue);
}
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate called"); Log.i(TAG, "onCreate called");
...@@ -332,18 +351,15 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -332,18 +351,15 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
mChart.getGridLabelRenderer().setHumanRounding(false); mChart.getGridLabelRenderer().setHumanRounding(false);
mChart.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() { mChart.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() {
DateFormat mFormat = DateFormat.getTimeInstance(DateFormat.SHORT); DateFormat mFormat = new SimpleDateFormat("HH:mm"); // 24 hour format forced to fix label overlap
@Override @Override
public String formatLabel(double value, boolean isValueX) { public String formatLabel(double value, boolean isValueX) {
if (isValueX) { if (isValueX) {
return mFormat.format(new Date((long) value)); return mFormat.format(new Date((long) value));
} else {
if (mmolxl) {
return sgvFormatter.format(value / MMOLXLFACTOR);
} else { } else {
return sgvFormatter.format(value); return sgvFormatter.format(value);
} }
}
}} }}
); );
} }
...@@ -617,7 +633,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -617,7 +633,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
lastQueryTS = pump.getLastQueryTS(); lastQueryTS = pump.getLastQueryTS();
startCgmService(MainActivity.getNextPoll(pumpStatusData)); // >>>>> note: prototype smart poll handling added to cnl intent
// startCgmService(MainActivity.getNextPoll(pumpStatusData));
// Delete invalid or old records from Realm // Delete invalid or old records from Realm
// TODO - show an error message if the valid records haven't been uploaded // TODO - show an error message if the valid records haven't been uploaded
...@@ -640,7 +657,10 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -640,7 +657,10 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
} }
// TODO - handle isOffline in NightscoutUploadIntentService? // TODO - handle isOffline in NightscoutUploadIntentService?
uploadCgmData();
// >>>>> check this out as it's uploading before cnl comms finishes and may cause occasional channel changes due to wifi noise - cnl intent handles ns upload trigger after all comms finish
// uploadCgmData();
refreshDisplay(); refreshDisplay();
} }
}); });
...@@ -685,7 +705,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -685,7 +705,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
} }
} }
private Queue<StatusMessage> messages = new ArrayBlockingQueue<>(10); private Queue<StatusMessage> messages = new ArrayBlockingQueue<>(400);
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
...@@ -693,7 +713,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -693,7 +713,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
Log.i(TAG, "Message Receiver: " + message); Log.i(TAG, "Message Receiver: " + message);
synchronized (messages) { synchronized (messages) {
while (messages.size() > 8) { while (messages.size() > 398) {
messages.poll(); messages.poll();
} }
messages.add(new StatusMessage(message)); messages.add(new StatusMessage(message));
...@@ -802,6 +822,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -802,6 +822,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
} }
private void updateChart(RealmResults<PumpStatusEvent> results) { private void updateChart(RealmResults<PumpStatusEvent> results) {
mChart.getGridLabelRenderer().setNumHorizontalLabels(6);
int size = results.size(); int size = results.size();
if (size == 0) { if (size == 0) {
final long now = System.currentTimeMillis(), final long now = System.currentTimeMillis(),
...@@ -832,9 +855,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -832,9 +855,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
int sgv = pumpStatus.getSgv(); int sgv = pumpStatus.getSgv();
if (mmolxl) { if (mmolxl) {
entries[pos++] = new DataPoint(pumpStatus.getEventDate(), pumpStatus.getSgv() / MMOLXLFACTOR); entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv() / MMOLXLFACTOR);
} else { } else {
entries[pos++] = new DataPoint(pumpStatus.getEventDate(), pumpStatus.getSgv()); entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv());
} }
} }
...@@ -860,11 +883,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -860,11 +883,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
double sgv = dataPoint.getY(); double sgv = dataPoint.getY();
StringBuilder sb = new StringBuilder(mFormat.format(new Date((long) dataPoint.getX())) + ": "); StringBuilder sb = new StringBuilder(mFormat.format(new Date((long) dataPoint.getX())) + ": ");
if (mmolxl) {
sb.append(sgvFormatter.format(sgv / MMOLXLFACTOR));
} else {
sb.append(sgvFormatter.format(sgv)); sb.append(sgvFormatter.format(sgv));
}
Toast.makeText(getBaseContext(), sb.toString(), Toast.LENGTH_SHORT).show(); Toast.makeText(getBaseContext(), sb.toString(), Toast.LENGTH_SHORT).show();
} }
}); });
...@@ -873,11 +892,11 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc ...@@ -873,11 +892,11 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc
@Override @Override
public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) { public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) {
double sgv = dataPoint.getY(); double sgv = dataPoint.getY();
if (sgv < 80) if (sgv < (mmolxl?4.5:80))
paint.setColor(Color.RED); paint.setColor(Color.RED);
else if (sgv <= 180) else if (sgv <= (mmolxl?10:180))
paint.setColor(Color.GREEN); paint.setColor(Color.GREEN);
else if (sgv <= 260) else if (sgv <= (mmolxl?14:260))
paint.setColor(Color.YELLOW); paint.setColor(Color.YELLOW);
else else
paint.setColor(Color.RED); paint.setColor(Color.RED);
......
...@@ -9,7 +9,6 @@ import java.security.NoSuchAlgorithmException; ...@@ -9,7 +9,6 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.USB.UsbHidDriver;
...@@ -51,6 +50,8 @@ public class MedtronicCnlReader { ...@@ -51,6 +50,8 @@ public class MedtronicCnlReader {
private MedtronicCnlSession mPumpSession = new MedtronicCnlSession(); private MedtronicCnlSession mPumpSession = new MedtronicCnlSession();
private String mStickSerial = null; private String mStickSerial = null;
private static final int SLEEP_MS = 500;
public MedtronicCnlReader(UsbHidDriver device) { public MedtronicCnlReader(UsbHidDriver device) {
mDevice = device; mDevice = device;
} }
...@@ -78,9 +79,9 @@ public class MedtronicCnlReader { ...@@ -78,9 +79,9 @@ public class MedtronicCnlReader {
doRetry = false; doRetry = false;
try { try {
new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.NAK) new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.NAK)
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.EOT); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.EOT);
new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.ENQ) new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.ENQ)
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
} catch (UnexpectedMessageException e2) { } catch (UnexpectedMessageException e2) {
try { try {
new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT).send(mDevice); new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT).send(mDevice);
...@@ -95,11 +96,11 @@ public class MedtronicCnlReader { ...@@ -95,11 +96,11 @@ public class MedtronicCnlReader {
public void enterPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { public void enterPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException {
Log.d(TAG, "Begin enterPasshtroughMode"); Log.d(TAG, "Begin enterPasshtroughMode");
new ContourNextLinkCommandMessage("W|") new ContourNextLinkCommandMessage("W|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
new ContourNextLinkCommandMessage("Q|") new ContourNextLinkCommandMessage("Q|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
new ContourNextLinkCommandMessage("1|") new ContourNextLinkCommandMessage("1|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
Log.d(TAG, "Finished enterPasshtroughMode"); Log.d(TAG, "Finished enterPasshtroughMode");
} }
...@@ -150,9 +151,11 @@ public class MedtronicCnlReader { ...@@ -150,9 +151,11 @@ public class MedtronicCnlReader {
ChannelNegotiateResponseMessage response = new ChannelNegotiateRequestMessage(mPumpSession).send(mDevice); ChannelNegotiateResponseMessage response = new ChannelNegotiateRequestMessage(mPumpSession).send(mDevice);
if (response.getRadioChannel() == mPumpSession.getRadioChannel()) { if (response.getRadioChannel() == mPumpSession.getRadioChannel()) {
mPumpSession.setRadioRSSI(response.getRadioRSSI());
break; break;
} else { } else {
mPumpSession.setRadioChannel((byte)0); mPumpSession.setRadioChannel((byte)0);
mPumpSession.setRadioRSSI((byte)0);
} }
} }
...@@ -168,7 +171,17 @@ public class MedtronicCnlReader { ...@@ -168,7 +171,17 @@ public class MedtronicCnlReader {
public Date getPumpTime() throws EncryptionException, IOException, ChecksumException, TimeoutException, UnexpectedMessageException { public Date getPumpTime() throws EncryptionException, IOException, ChecksumException, TimeoutException, UnexpectedMessageException {
Log.d(TAG, "Begin getPumpTime"); Log.d(TAG, "Begin getPumpTime");
// FIXME - throw if not in EHSM mode (add a state machine)
// CNL<-->PUMP comms can have occasional short lived noise causing errors, retrying once catches this
try {
PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice);
Log.d(TAG, "Finished getPumpTime with date " + response.getPumpTime());
return response.getPumpTime();
} catch (UnexpectedMessageException e) {
Log.e(TAG, "Unexpected Message", e);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout communicating with the Contour Next Link.", e);
}
PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice); PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice);
...@@ -179,7 +192,18 @@ public class MedtronicCnlReader { ...@@ -179,7 +192,18 @@ public class MedtronicCnlReader {
public PumpStatusEvent updatePumpStatus(PumpStatusEvent pumpRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException, UnexpectedMessageException { public PumpStatusEvent updatePumpStatus(PumpStatusEvent pumpRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException, UnexpectedMessageException {
Log.d(TAG, "Begin updatePumpStatus"); Log.d(TAG, "Begin updatePumpStatus");
// FIXME - throw if not in EHSM mode (add a state machine) // CNL<-->PUMP comms can have occasional short lived noise causing errors, retrying once catches this
try {
PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice);
response.updatePumpRecord(pumpRecord);
Log.d(TAG, "Finished updatePumpStatus");
return pumpRecord;
} catch (UnexpectedMessageException e) {
Log.e(TAG, "Unexpected Message", e);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout communicating with the Contour Next Link.", e);
}
PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice); PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice);
response.updatePumpRecord(pumpRecord); response.updatePumpRecord(pumpRecord);
...@@ -222,18 +246,18 @@ public class MedtronicCnlReader { ...@@ -222,18 +246,18 @@ public class MedtronicCnlReader {
public void endPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { public void endPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException {
Log.d(TAG, "Begin endPassthroughMode"); Log.d(TAG, "Begin endPassthroughMode");
new ContourNextLinkCommandMessage("W|") new ContourNextLinkCommandMessage("W|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
new ContourNextLinkCommandMessage("Q|") new ContourNextLinkCommandMessage("Q|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
new ContourNextLinkCommandMessage("0|") new ContourNextLinkCommandMessage("0|")
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK);
Log.d(TAG, "Finished endPassthroughMode"); Log.d(TAG, "Finished endPassthroughMode");
} }
public void endControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { public void endControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException {
Log.d(TAG, "Begin endControlMode"); Log.d(TAG, "Begin endControlMode");
new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT) new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT)
.send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ENQ); .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ENQ);
Log.d(TAG, "Finished endControlMode"); Log.d(TAG, "Finished endControlMode");
} }
} }
...@@ -20,6 +20,8 @@ public class MedtronicCnlSession { ...@@ -20,6 +20,8 @@ public class MedtronicCnlSession {
private long pumpMAC; private long pumpMAC;
private byte radioChannel; private byte radioChannel;
private byte radioRSSI;
private int bayerSequenceNumber = 1; private int bayerSequenceNumber = 1;
private int medtronicSequenceNumber = 1; private int medtronicSequenceNumber = 1;
...@@ -80,6 +82,14 @@ public class MedtronicCnlSession { ...@@ -80,6 +82,14 @@ public class MedtronicCnlSession {
return radioChannel; return radioChannel;
} }
public byte getRadioRSSI() {
return radioRSSI;
}
public int getRadioRSSIpercentage() {
return (((int) radioRSSI & 0x00FF) * 100) / 0xA8;
}
public void incrBayerSequenceNumber() { public void incrBayerSequenceNumber() {
bayerSequenceNumber++; bayerSequenceNumber++;
} }
...@@ -92,6 +102,10 @@ public class MedtronicCnlSession { ...@@ -92,6 +102,10 @@ public class MedtronicCnlSession {
this.radioChannel = radioChannel; this.radioChannel = radioChannel;
} }
public void setRadioRSSI(byte radioRSSI) {
this.radioRSSI = radioRSSI;
}
public void setHMAC(byte[] hmac) { public void setHMAC(byte[] hmac) {
this.HMAC = hmac; this.HMAC = hmac;
} }
......
...@@ -16,6 +16,7 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon ...@@ -16,6 +16,7 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon
private static final String TAG = ChannelNegotiateResponseMessage.class.getSimpleName(); private static final String TAG = ChannelNegotiateResponseMessage.class.getSimpleName();
private byte radioChannel = 0; private byte radioChannel = 0;
private byte radioRSSI = 0;
protected ChannelNegotiateResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, IOException { protected ChannelNegotiateResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, IOException {
super(payload); super(payload);
...@@ -25,15 +26,21 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon ...@@ -25,15 +26,21 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon
Log.d(TAG, "negotiateChannel: Check response length"); Log.d(TAG, "negotiateChannel: Check response length");
if (responseBytes.length > 46) { if (responseBytes.length > 46) {
radioChannel = responseBytes[76]; radioChannel = responseBytes[76];
radioRSSI = responseBytes[59];
if (responseBytes[76] != pumpSession.getRadioChannel()) { if (responseBytes[76] != pumpSession.getRadioChannel()) {
throw new IOException(String.format(Locale.getDefault(), "Expected to get a message for channel %d. Got %d", pumpSession.getRadioChannel(), responseBytes[76])); throw new IOException(String.format(Locale.getDefault(), "Expected to get a message for channel %d. Got %d", pumpSession.getRadioChannel(), responseBytes[76]));
} }
} else { } else {
radioChannel = ((byte) 0); radioChannel = ((byte) 0);
radioRSSI = ((byte) 0);
} }
} }
public byte getRadioChannel() { public byte getRadioChannel() {
return radioChannel; return radioChannel;
} }
public byte getRadioRSSI() {
return radioRSSI;
}
} }
...@@ -8,9 +8,7 @@ import java.nio.ByteBuffer; ...@@ -8,9 +8,7 @@ import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.USB.UsbHidDriver;
import info.nightscout.android.medtronic.exception.ChecksumException; import info.nightscout.android.medtronic.MainActivity;
import info.nightscout.android.medtronic.exception.EncryptionException;
import info.nightscout.android.medtronic.exception.UnexpectedMessageException;
import info.nightscout.android.utils.HexDump; import info.nightscout.android.utils.HexDump;
/** /**
...@@ -20,7 +18,7 @@ public abstract class ContourNextLinkMessage { ...@@ -20,7 +18,7 @@ public abstract class ContourNextLinkMessage {
private static final String TAG = ContourNextLinkMessage.class.getSimpleName(); private static final String TAG = ContourNextLinkMessage.class.getSimpleName();
private static final int USB_BLOCKSIZE = 64; private static final int USB_BLOCKSIZE = 64;
private static final int READ_TIMEOUT_MS = 10000; private static final int READ_TIMEOUT_MS = 15000; //ASTM standard is 15 seconds (note was previously set at 10 seconds)
private static final String BAYER_USB_HEADER = "ABC"; private static final String BAYER_USB_HEADER = "ABC";
protected ByteBuffer mPayload; protected ByteBuffer mPayload;
...@@ -142,6 +140,56 @@ public abstract class ContourNextLinkMessage { ...@@ -142,6 +140,56 @@ public abstract class ContourNextLinkMessage {
return responseMessage.toByteArray(); return responseMessage.toByteArray();
} }
// safety check to make sure a expected 0x81 response is received before next expected 0x80 response
// very infrequent as clearMessage catches most issues but very important to save a CNL error situation
protected int readMessage_0x81(UsbHidDriver mDevice) throws IOException, TimeoutException {
int responseSize = 0;
boolean doRetry;
do {
byte[] responseBytes = readMessage(mDevice);
if (responseBytes[18] != (byte) 0x81) {
doRetry = true;
Log.d(TAG, "readMessage0x81: did not get 0x81 response, got " + responseBytes[18]);
} else {
doRetry = false;
responseSize = responseBytes.length;
}
} while (doRetry);
return responseSize;
}
// intercept unexpected messages from the CNL
// these usually come from pump requests as it can occasionally resend message responses several times (possibly due to a missed CNL ACK during CNL-PUMP comms?)
// mostly noted on the higher radio channels, channel 26 shows this the most
// if these messages are not cleared the CNL will likely error needing to be unplugged to reset as it expects them to be read before any further commands are sent
protected int clearMessage(UsbHidDriver mDevice) throws IOException {
byte[] responseBuffer = new byte[USB_BLOCKSIZE];
int bytesRead;
int bytesClear = 0;
do {
bytesRead = mDevice.read(responseBuffer, 2000);
if (bytesRead > 0) {
bytesClear += bytesRead;
String responseString = HexDump.dumpHexString(responseBuffer);
Log.d(TAG, "READ: " + responseString);
}
} while (bytesRead > 0);
if (bytesClear > 0) {
Log.d(TAG, "clearMessage: message stream cleared bytes: " + bytesClear);
}
return bytesClear;
}
public enum ASCII { public enum ASCII {
STX(0x02), STX(0x02),
EOT(0x04), EOT(0x04),
......
...@@ -20,6 +20,10 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext ...@@ -20,6 +20,10 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext
@Override @Override
public ContourNextLinkResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, UnexpectedMessageException { public ContourNextLinkResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, UnexpectedMessageException {
// clear unexpected incoming messages
clearMessage(mDevice);
sendMessage(mDevice); sendMessage(mDevice);
if (millis > 0) { if (millis > 0) {
try { try {
...@@ -27,11 +31,17 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext ...@@ -27,11 +31,17 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext
} catch (InterruptedException e) { } catch (InterruptedException e) {
} }
} }
// The End EHSM Session only has an 0x81 response // The End EHSM Session only has an 0x81 response
if (readMessage_0x81(mDevice) != 48) {
throw new UnexpectedMessageException("length of EHSMMessage response does not match");
}
/*
readMessage(mDevice); readMessage(mDevice);
if (this.encode().length != 54) { if (this.encode().length != 54) {
throw new UnexpectedMessageException("length of EHSMMessage response does not match"); throw new UnexpectedMessageException("length of EHSMMessage response does not match");
} }
*/
return null; return null;
} }
} }
...@@ -22,6 +22,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage ...@@ -22,6 +22,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage
} }
public PumpStatusResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { public PumpStatusResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException {
sendMessage(mDevice); sendMessage(mDevice);
if (millis > 0) { if (millis > 0) {
try { try {
...@@ -31,7 +32,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage ...@@ -31,7 +32,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage
} }
} }
// Read the 0x81 // Read the 0x81
readMessage(mDevice); readMessage_0x81(mDevice);
if (millis > 0) { if (millis > 0) {
try { try {
Log.d(TAG, "waiting " + millis +" ms"); Log.d(TAG, "waiting " + millis +" ms");
...@@ -41,6 +42,9 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage ...@@ -41,6 +42,9 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage
} }
PumpStatusResponseMessage response = this.getResponse(readMessage(mDevice)); PumpStatusResponseMessage response = this.getResponse(readMessage(mDevice));
// clear unexpected incoming messages
clearMessage(mDevice);
return response; return response;
} }
......
...@@ -19,6 +19,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P ...@@ -19,6 +19,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P
@Override @Override
public PumpTimeResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { public PumpTimeResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException {
sendMessage(mDevice); sendMessage(mDevice);
if (millis > 0) { if (millis > 0) {
try { try {
...@@ -27,7 +28,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P ...@@ -27,7 +28,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P
} }
} }
// Read the 0x81 // Read the 0x81
readMessage(mDevice); readMessage_0x81(mDevice);
if (millis > 0) { if (millis > 0) {
try { try {
Thread.sleep(millis); Thread.sleep(millis);
...@@ -37,6 +38,9 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P ...@@ -37,6 +38,9 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P
// Read the 0x80 // Read the 0x80
PumpTimeResponseMessage response = this.getResponse(readMessage(mDevice)); PumpTimeResponseMessage response = this.getResponse(readMessage(mDevice));
// Pump sends additional 0x80 message when not using EHSM, lets clear this and any unexpected incoming messages
clearMessage(mDevice);
return response; return response;
} }
......
...@@ -10,6 +10,7 @@ import info.nightscout.android.BuildConfig; ...@@ -10,6 +10,7 @@ import info.nightscout.android.BuildConfig;
import info.nightscout.android.medtronic.MedtronicCnlSession; import info.nightscout.android.medtronic.MedtronicCnlSession;
import info.nightscout.android.medtronic.exception.ChecksumException; import info.nightscout.android.medtronic.exception.ChecksumException;
import info.nightscout.android.medtronic.exception.EncryptionException; import info.nightscout.android.medtronic.exception.EncryptionException;
import info.nightscout.android.medtronic.exception.UnexpectedMessageException;
import info.nightscout.android.utils.HexDump; import info.nightscout.android.utils.HexDump;
/** /**
...@@ -20,14 +21,14 @@ public class PumpTimeResponseMessage extends MedtronicSendMessageResponseMessage ...@@ -20,14 +21,14 @@ public class PumpTimeResponseMessage extends MedtronicSendMessageResponseMessage
private Date pumpTime; private Date pumpTime;
protected PumpTimeResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { protected PumpTimeResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException {
super(pumpSession, payload); super(pumpSession, payload);
if (this.encode().length < (61 + 8)) { if (this.encode().length < (61 + 8)) {
// Invalid message. Return an invalid date. // Invalid message. Return an invalid date.
// TODO - deal with this more elegantly // TODO - deal with this more elegantly
Log.e(TAG, "Invalid message received for getPumpTime"); Log.e(TAG, "Invalid message received for getPumpTime");
pumpTime = new Date(); throw new UnexpectedMessageException("Invalid message received for getPumpTime");
} else { } else {
ByteBuffer dateBuffer = ByteBuffer.allocate(8); ByteBuffer dateBuffer = ByteBuffer.allocate(8);
dateBuffer.order(ByteOrder.BIG_ENDIAN); dateBuffer.order(ByteOrder.BIG_ENDIAN);
......
...@@ -80,7 +80,8 @@ public class MedtronicCnlAlarmManager { ...@@ -80,7 +80,8 @@ public class MedtronicCnlAlarmManager {
// restarting the alarm after MedtronicCnlIntentService.POLL_PERIOD_MS from now // restarting the alarm after MedtronicCnlIntentService.POLL_PERIOD_MS from now
public static void restartAlarm() { public static void restartAlarm() {
setAlarmAfterMillis(MainActivity.pollInterval + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS); //setAlarmAfterMillis(MainActivity.pollInterval + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS);
setAlarmAfterMillis(MainActivity.pollInterval); // grace already accounted for when using current intent time to set default restart
} }
// Cancel the alarm. // Cancel the alarm.
......
...@@ -20,6 +20,8 @@ import java.security.NoSuchAlgorithmException; ...@@ -20,6 +20,8 @@ import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import info.nightscout.android.R; import info.nightscout.android.R;
import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.USB.UsbHidDriver;
...@@ -98,6 +100,25 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -98,6 +100,25 @@ public class MedtronicCnlIntentService extends IntentService {
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
Log.d(TAG, "onHandleIntent called"); Log.d(TAG, "onHandleIntent called");
long timePollStarted = System.currentTimeMillis();
long timePollExpected = timePollStarted;
if (MainActivity.timeLastGoodSGV != 0) {
timePollExpected = MainActivity.timeLastGoodSGV + POLL_PERIOD_MS + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((timePollStarted - 1000L - (MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS));
}
// avoid polling when too close to sensor-pump comms
if (((timePollExpected - timePollStarted) > 5000L) && ((timePollExpected - timePollStarted) < (POLL_GRACE_PERIOD_MS + 45000L))) {
sendStatus("Please wait: Poll due in " + ((timePollExpected - timePollStarted) / 1000L) + " seconds");
MedtronicCnlAlarmManager.setAlarm(timePollExpected);
MedtronicCnlAlarmReceiver.completeWakefulIntent(intent);
return;
}
long pollInterval = MainActivity.pollInterval;
if ((MainActivity.pumpBattery > 0) && (MainActivity.pumpBattery <= 25)) {
pollInterval = MainActivity.lowBatteryPollInterval;
}
if (!hasUsbHostFeature()) { if (!hasUsbHostFeature()) {
sendStatus("It appears that this device doesn't support USB OTG."); sendStatus("It appears that this device doesn't support USB OTG.");
Log.e(TAG, "Device does not support USB OTG"); Log.e(TAG, "Device does not support USB OTG");
...@@ -135,14 +156,16 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -135,14 +156,16 @@ public class MedtronicCnlIntentService extends IntentService {
return; return;
} }
DateFormat df = new SimpleDateFormat("HH:mm:ss");
MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice); MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice);
Realm realm = Realm.getDefaultInstance(); Realm realm = Realm.getDefaultInstance();
realm.beginTransaction(); realm.beginTransaction();
try { try {
sendStatus("Connecting to the Contour Next Link..."); sendStatus("Connecting to Contour Next Link");
Log.d(TAG, "Connecting to the Contour Next Link."); Log.d(TAG, "Connecting to Contour Next Link");
cnlReader.requestDeviceInfo(); cnlReader.requestDeviceInfo();
// Is the device already configured? // Is the device already configured?
...@@ -162,6 +185,7 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -162,6 +185,7 @@ public class MedtronicCnlIntentService extends IntentService {
try { try {
cnlReader.enterPassthroughMode(); cnlReader.enterPassthroughMode();
cnlReader.openConnection(); cnlReader.openConnection();
cnlReader.requestReadInfo(); cnlReader.requestReadInfo();
String key = info.getKey(); String key = info.getKey();
...@@ -193,17 +217,12 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -193,17 +217,12 @@ public class MedtronicCnlIntentService extends IntentService {
if (radioChannel == 0) { if (radioChannel == 0) {
sendStatus("Could not communicate with the 640g. Are you near the pump?"); sendStatus("Could not communicate with the 640g. Are you near the pump?");
Log.i(TAG, "Could not communicate with the 640g. Are you near the pump?"); Log.i(TAG, "Could not communicate with the 640g. Are you near the pump?");
pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L); // reduce polling interval to half until pump is available
// reduce polling interval to half until pump is available
MedtronicCnlAlarmManager.setAlarm(activePump.getLastQueryTS() +
(MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L))
);
} else { } else {
setActivePumpMac(pumpMAC); setActivePumpMac(pumpMAC);
activePump.setLastRadioChannel(radioChannel); activePump.setLastRadioChannel(radioChannel);
sendStatus(String.format(Locale.getDefault(), "Connected to Contour Next Link on channel %d.", (int) radioChannel)); sendStatus(String.format(Locale.getDefault(), "Connected on channel %d RSSI: %d%%", (int) radioChannel, cnlReader.getPumpSession().getRadioRSSIpercentage()));
Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel)); Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel));
cnlReader.beginEHSMSession();
// read pump status // read pump status
PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class); PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class);
...@@ -223,13 +242,28 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -223,13 +242,28 @@ public class MedtronicCnlIntentService extends IntentService {
pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset)); pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset));
cnlReader.updatePumpStatus(pumpRecord); cnlReader.updatePumpStatus(pumpRecord);
cnlReader.endEHSMSession();
if (pumpRecord.getSgv() != 0) { if (pumpRecord.getSgv() != 0) {
String offsetSign = "";
if (pumpOffset > 0) {
offsetSign = "+";
}
sendStatus("SGV: " + MainActivity.strFormatSGV(pumpRecord.getSgv()) + " At: " + df.format(pumpRecord.getEventDate().getTime()) + " Pump: " + offsetSign + (pumpOffset / 1000L) + "sec"); //note: event time is currently stored with offset
// Check if pump sent old event when new expected and schedule a re-poll
if (((pumpRecord.getEventDate().getTime() - MainActivity.timeLastGoodSGV) < 5000L) && ((timePollExpected - timePollStarted) < 5000L)) {
pollInterval = 90000L; // polling interval set to 90 seconds
sendStatus("Pump sent old SGV event, re-polling...");
}
MainActivity.timeLastGoodSGV = pumpRecord.getEventDate().getTime(); // track last good sgv event time
MainActivity.pumpBattery = pumpRecord.getBatteryPercentage(); // track pump battery
MainActivity.countUnavailableSGV = 0; // reset unavailable sgv count
// Check that the record doesn't already exist before committing // Check that the record doesn't already exist before committing
RealmResults<PumpStatusEvent> checkExistingRecords = activePump.getPumpHistory() RealmResults<PumpStatusEvent> checkExistingRecords = activePump.getPumpHistory()
.where() .where()
.equalTo("eventDate", pumpRecord.getEventDate()) .equalTo("eventDate", pumpRecord.getEventDate()) // >>>>>>> check as event date may not = exact pump event date due to it being stored with offset added this could lead to dup events due to slight variability in time offset
.equalTo("sgv", pumpRecord.getSgv()) .equalTo("sgv", pumpRecord.getSgv())
.findAll(); .findAll();
...@@ -240,15 +274,24 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -240,15 +274,24 @@ public class MedtronicCnlIntentService extends IntentService {
Log.d(TAG, "history reading size: " + activePump.getPumpHistory().size()); Log.d(TAG, "history reading size: " + activePump.getPumpHistory().size());
Log.d(TAG, "history reading date: " + activePump.getPumpHistory().last().getEventDate()); Log.d(TAG, "history reading date: " + activePump.getPumpHistory().last().getEventDate());
} else {
sendStatus("SGV: unavailable from pump");
MainActivity.countUnavailableSGV ++; // poll clash detection
} }
realm.commitTransaction(); realm.commitTransaction();
// Tell the Main Activity we have new data // Tell the Main Activity we have new data
sendMessage(Constants.ACTION_UPDATE_PUMP); sendMessage(Constants.ACTION_UPDATE_PUMP);
} }
} catch (UnexpectedMessageException e) { } catch (UnexpectedMessageException e) {
Log.e(TAG, "Unexpected Message", e); Log.e(TAG, "Unexpected Message", e);
sendStatus("Communication Error: " + e.getMessage()); sendStatus("Communication Error: " + e.getMessage());
pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout communicating with the Contour Next Link.", e);
sendStatus("Timeout communicating with the Contour Next Link.");
pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
Log.e(TAG, "Could not determine CNL HMAC", e); Log.e(TAG, "Could not determine CNL HMAC", e);
sendStatus("Error connecting to Contour Next Link: Hashing error."); sendStatus("Error connecting to Contour Next Link: Hashing error.");
...@@ -283,10 +326,34 @@ public class MedtronicCnlIntentService extends IntentService { ...@@ -283,10 +326,34 @@ public class MedtronicCnlIntentService extends IntentService {
} }
realm.close(); realm.close();
} }
// TODO - set status if offline or Nightscout not reachable // TODO - set status if offline or Nightscout not reachable
sendToXDrip(); sendToXDrip();
uploadToNightscout(); uploadToNightscout();
// smart polling and pump-sensor poll clash detection
long lastActualPollTime = timePollStarted;
if (MainActivity.timeLastGoodSGV > 0) {
lastActualPollTime = MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((System.currentTimeMillis() - (MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS));
}
long nextActualPollTime = lastActualPollTime + POLL_PERIOD_MS;
long nextRequestedPollTime = lastActualPollTime + pollInterval;
if ((nextRequestedPollTime - System.currentTimeMillis()) < 10000L) {
nextRequestedPollTime = nextActualPollTime;
}
// extended unavailable SGV may be due to clash with the current polling time
// while we wait for a good SGV event, polling is auto adjusted by offsetting the next poll based on miss count
if (MainActivity.countUnavailableSGV > 0) {
if (MainActivity.timeLastGoodSGV == 0) {
nextRequestedPollTime += POLL_PERIOD_MS / 5L; // if there is a uploader/sensor poll clash on startup then this will push the next attempt out by 60 seconds
}
else if (MainActivity.countUnavailableSGV > 2) {
sendStatus("Warning: No SGV available from pump for " + MainActivity.countUnavailableSGV + " attempts");
nextRequestedPollTime += ((long) ((MainActivity.countUnavailableSGV - 2) % 5)) * (POLL_PERIOD_MS / 10L); // adjust poll time in 1/10 steps to avoid potential poll clash (max adjustment at 5/10)
}
}
MedtronicCnlAlarmManager.setAlarm(nextRequestedPollTime);
sendStatus("Next poll due at: " + df.format(nextRequestedPollTime));
MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); MedtronicCnlAlarmReceiver.completeWakefulIntent(intent);
} }
} }
......
...@@ -70,6 +70,7 @@ public class XDripPlusUploadIntentService extends IntentService { ...@@ -70,6 +70,7 @@ public class XDripPlusUploadIntentService extends IntentService {
List<PumpStatusEvent> records = all_records.subList(0, 1); List<PumpStatusEvent> records = all_records.subList(0, 1);
doXDripUpload(records); doXDripUpload(records);
} }
mRealm.close();
XDripPlusUploadReceiver.completeWakefulIntent(intent); XDripPlusUploadReceiver.completeWakefulIntent(intent);
} }
......
...@@ -128,18 +128,21 @@ ...@@ -128,18 +128,21 @@
android:id="@+id/scrollView" android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="fill_parent"> android:layout_height="fill_parent">
android:gravity="bottom"
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
android:gravity="bottom"
<TextView <TextView
android:id="@+id/textview_log" android:id="@+id/textview_log"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10sp" android:layout_margin="10sp"
android:maxLines="20" android:maxLines="800"
android:gravity="bottom"
android:text="" /> android:text="" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment