diff --git a/lib/bluetooth-scanner b/lib/bluetooth-scanner new file mode 160000 index 0000000..915ceb6 --- /dev/null +++ b/lib/bluetooth-scanner @@ -0,0 +1 @@ +Subproject commit 915ceb61c8babd079c15795a52589d3f1ce84bdc diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeRxMessage.java new file mode 100644 index 0000000..2b34836 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeRxMessage.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Created by joeginley on 3/16/16. + */ +public class AuthChallengeRxMessage extends BaseMessage { + public static final int opcode = 0x03; + public byte[] tokenHash; + public byte[] challenge; + private final static String TAG = G5CollectionService.TAG; // meh + public AuthChallengeRxMessage(byte[] data) { + UserError.Log.d(TAG,"AuthChallengeRX: "+ JoH.bytesToHex(data)); + if (data.length >= 17) { + if (data[0] == opcode) { + tokenHash = Arrays.copyOfRange(data, 1, 9); + challenge = Arrays.copyOfRange(data, 9, 17); + } + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeTxMessage.java new file mode 100644 index 0000000..0145c09 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthChallengeTxMessage.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.G5Model; +import com.eveningoutpost.dexdrip.G5Model.TransmitterMessage; +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by joeginley on 3/16/16. + */ +public class AuthChallengeTxMessage extends TransmitterMessage { + byte opcode = 0x04; + byte[] challengeHash; + + public AuthChallengeTxMessage(byte[] challenge) { + challengeHash = challenge; + + data = ByteBuffer.allocate(9); + data.put(opcode); + data.put(challengeHash); + + byteSequence = data.array(); + UserError.Log.d(TAG,"AuthChallengeTX: "+ JoH.bytesToHex(byteSequence)); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthRequestTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthRequestTxMessage.java new file mode 100644 index 0000000..338ef21 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthRequestTxMessage.java @@ -0,0 +1,50 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.UUID; + +import static com.eveningoutpost.dexdrip.utils.CipherUtils.getRandomKey; + +/** + * Created by joeginley on 3/16/16. + */ +@SuppressWarnings("FieldCanBeLocal") +public class AuthRequestTxMessage extends BaseMessage { + public final byte opcode = 0x01; + public byte[] singleUseToken; + private final byte endByteStd = 0x2; + private final byte endByteAlt = 0x1; + + + public AuthRequestTxMessage(int token_size) { + this(token_size, false); + } + + public AuthRequestTxMessage(int token_size, boolean alt) { + byte[] uuidBytes = getRandomKey(); + final UUID uuid = UUID.nameUUIDFromBytes(uuidBytes); + + try { + uuidBytes = uuid.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + final ByteBuffer bb = ByteBuffer.allocate(token_size); + bb.put(uuidBytes, 0, token_size); + singleUseToken = bb.array(); + + data = ByteBuffer.allocate(token_size + 2); + data.put(opcode); + data.put(singleUseToken); + data.put(alt ? endByteAlt : endByteStd); + + byteSequence = data.array(); + UserError.Log.d(TAG, "New AuthRequestTxMessage: " + JoH.bytesToHex(byteSequence)); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthStatusRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthStatusRxMessage.java new file mode 100644 index 0000000..2c2e4d2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/AuthStatusRxMessage.java @@ -0,0 +1,35 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +import com.eveningoutpost.dexdrip.Models.UserError; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by joeginley on 3/16/16. + */ +public class AuthStatusRxMessage extends BaseMessage { + public static final int opcode = 0x5; + public int authenticated; + public int bonded; + + public AuthStatusRxMessage(byte[] packet) { + if (packet.length >= 3) { + if (packet[0] == opcode) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + + authenticated = data.get(1); + bonded = data.get(2); + UserError.Log.d(TAG,"AuthRequestRxMessage: authenticated:"+authenticated+" bonded:"+bonded); + } + } + } + + public boolean isAuthenticated() { + return authenticated == 1; + } + public boolean isBonded() { + return bonded == 1; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillRxMessage.java new file mode 100644 index 0000000..d85b46d --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillRxMessage.java @@ -0,0 +1,30 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// created by jamorham + +public class BackFillRxMessage extends BaseMessage { + + public static final int opcode = 0x51; + private static final int length = 20; + + private boolean valid = false; + + BackFillRxMessage(byte[] packet) { + + if (packet.length == length) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + valid = true; + // meh + // 51 00 01 01 A1A00200 DDAF0200 82000000 2361 1625 + } + } + } + + public boolean valid() { + return valid; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillStream.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillStream.java new file mode 100644 index 0000000..1b9f9d9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillStream.java @@ -0,0 +1,131 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// created by jamorham + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.LinkedList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import static com.eveningoutpost.dexdrip.G5Model.DexTimeKeeper.fromDexTimeCached; + +public class BackFillStream extends BaseMessage { + + private int last_sequence = 0; + + BackFillStream() { + data = ByteBuffer.allocate(1000); + data.order(ByteOrder.LITTLE_ENDIAN); + } + + public synchronized void push(byte[] packet) { + + if (packet == null) return; + final int this_sequence = (int) packet[0]; + if (this_sequence == last_sequence + 1) { + last_sequence++; + + for (int i = 2; i < packet.length; i++) { + if (data.position() < data.limit()) { + data.put(packet[i]); + } else { + UserError.Log.wtf(TAG, "Reached limit for backfill stream size"); + } + } + } else { + UserError.Log.wtf(TAG, "Received backfill packet out of sequence: " + this_sequence + " vs " + (last_sequence + 1)); + } + } + + public List decode() { + final List backsies = new LinkedList<>(); + + int extent = data.position(); + data.rewind(); + final int length = data.getInt(); + // TODO check length + while (data.position() < extent) { + final int dexTime = data.getInt(); + final int glucose = data.getShort(); + final byte type = data.get(); + final byte trend = data.get(); + + final CalibrationState state = CalibrationState.parse(type); + + switch (state) { + case Ok: + case NeedsCalibration: + insertBackfillItem(backsies, dexTime, glucose, trend); + break; + + case WarmingUp: + break; + + case Errors: + /* This preference option has never been available outside of unit testing + and can now be removed. + if (Pref.getBooleanDefaultFalse("ob1_g5_use_errored_data")) { + insertBackfillItem(backsies, dexTime, glucose, trend); + } + */ + break; + + case InsufficientCalibration: + if (Pref.getBoolean("ob1_g5_use_insufficiently_calibrated", true)) { + insertBackfillItem(backsies, dexTime, glucose, trend); + } + break; + + case NeedsFirstCalibration: + case NeedsSecondCalibration: + case Unknown: + break; + + default: + UserError.Log.wtf(TAG, "Encountered backfill data we don't recognise: " + type + " " + glucose + " " + trend + " " + " " + JoH.dateTimeText(fromDexTimeCached(dexTime))); + break; + + } + } + return backsies; + + + } + + private void insertBackfillItem(List backsies, int dexTime, int glucose, byte trend) { + if (dexTime != 0) { + backsies.add(new Backsie(glucose, trend, dexTime)); + } + } + + public void enumerate(int size) { + + System.out.println("Size:" + size); + byte[] output = data.array(); + int i = 4; + while (i < data.position()) { + if ((i - 4) % size == 0) { + System.out.println(""); + } + System.out.print(JoH.bytesToHex(new byte[]{output[i]})); + i++; + } + System.out.println("\n"); + } + + @Data + @AllArgsConstructor() + public class Backsie { + private final int glucose; + private final int trend; + private final int dextime; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillTxMessage.java new file mode 100644 index 0000000..ceee907 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BackFillTxMessage.java @@ -0,0 +1,38 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +import static com.eveningoutpost.dexdrip.G5Model.DexTimeKeeper.getDexTime; + +// created by jamorham + +public class BackFillTxMessage extends BaseMessage { + + final byte opcode = 0x50; + final int length = 20; + + public BackFillTxMessage(int startDexTime, int endDexTime) { + init(opcode, length); + data.put((byte) 0x5); + data.put((byte) 0x2); + data.put((byte) 0x0); + data.putInt(startDexTime); + data.putInt(endDexTime); + data.put(new byte[6]); + appendCRC(); + UserError.Log.d(TAG, "BackfillTxMessage dbg: " + JoH.bytesToHex(byteSequence)); + } + + public static BackFillTxMessage get(String id, long startTime, long endTime) { + + final int dexStart = getDexTime(id, startTime); + final int dexEnd = getDexTime(id, endTime); + if (dexStart < 1 || dexEnd < 1) { + UserError.Log.e(TAG, "Unable to calculate start or end time for BackFillTxMessage"); + return null; + } + return new BackFillTxMessage(dexStart, dexEnd); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseAuthChallengeTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseAuthChallengeTxMessage.java new file mode 100644 index 0000000..7b7112e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseAuthChallengeTxMessage.java @@ -0,0 +1,18 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +// jamorham + +public class BaseAuthChallengeTxMessage extends BaseMessage { + static final byte opcode = 0x04; + + public BaseAuthChallengeTxMessage(final byte[] challenge) { + + init(opcode, 9); + data.put(challenge); + byteSequence = data.array(); + UserError.Log.d(TAG, "BaseAuthChallengeTX: " + JoH.bytesToHex(byteSequence)); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseGlucoseRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseGlucoseRxMessage.java new file mode 100644 index 0000000..f406e18 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseGlucoseRxMessage.java @@ -0,0 +1,52 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +import lombok.NoArgsConstructor; + +/** + * Created by jamorham on 02/07/2018. + * + */ + +@NoArgsConstructor +public abstract class BaseGlucoseRxMessage extends BaseMessage { + + private final static String TAG = G5CollectionService.TAG; // meh + + public TransmitterStatus status; + public int status_raw; + public int timestamp; + public int unfiltered; + public int filtered; + public int sequence; // : UInt32 + public boolean glucoseIsDisplayOnly; // : Bool + public int glucose; // : UInt16 + public int state; //: UInt8 + public int trend; // : Int8 127 = invalid + + + CalibrationState calibrationState() { + return CalibrationState.parse(state); + } + + boolean usable() { + return calibrationState().usableGlucose(); + } + + boolean insufficient() { + return calibrationState().insufficientCalibration(); + } + + boolean OkToCalibrate() { + return calibrationState().readyForCalibration(); + } + + public Double getTrend() { + return trend != 127 ? ((double) trend) / 10d : Double.NaN; + } + + public Integer getPredictedGlucose() { + return null; // stub + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseMessage.java new file mode 100644 index 0000000..c212ff7 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BaseMessage.java @@ -0,0 +1,174 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.chrispr.bluetooth.BluetoothScanner; +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +import com.google.gson.annotations.Expose; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +// jamorham + +public class BaseMessage { + protected static final String TAG = G5CollectionService.TAG; // meh + static final int INVALID_TIME = 0xFFFFFFFF; + @Expose + long postExecuteGuardTime = 50; + @Expose + public volatile byte[] byteSequence; + public ByteBuffer data; + + + void init(final byte opcode, final int length) { + data = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN); + data.put(opcode); + if (length == 1) { + getByteSequence(); + } else if (length == 3) { + appendCRC(); + } + } + + byte[] appendCRC() { + data.put(FastCRC16.calculate(getByteSequence(), byteSequence.length - 2)); + return getByteSequence(); + } + + boolean checkCRC(byte[] data) { + if ((data == null) || (data.length < 3)) return false; + final byte[] crc = FastCRC16.calculate(data, data.length - 2); + return crc[0] == data[data.length - 2] && crc[1] == data[data.length - 1]; + } + + byte[] getByteSequence() { + return byteSequence = data.array(); + } + + long guardTime() { + return postExecuteGuardTime; + } + + static long getUnsignedInt(ByteBuffer data) { + return ((data.get() & 0xff) + ((data.get() & 0xff) << 8) + ((data.get() & 0xff) << 16) + ((data.get() & 0xff) << 24)); + } + + static int getUnsignedShort(ByteBuffer data) { + return ((data.get() & 0xff) + ((data.get() & 0xff) << 8)); + } + + static int getUnsignedByte(ByteBuffer data) { + return ((data.get() & 0xff)); + } + + static String dottedStringFromData(ByteBuffer data, int length) { + + final byte[] bytes = new byte[length]; + data.get(bytes); + final StringBuilder sb = new StringBuilder(100); + for (byte x : bytes) { + if (sb.length() > 0) sb.append("."); + sb.append(String.format(Locale.US, "%d", (x & 0xff))); + } + return sb.toString(); + } + + static int getUnixTime() { + return (int) (JoH.tsl() / 1000); + } + + public static class G5CollectionService { + public static String TAG = "Message"; + } + + public static class UserError { + public static class Log { + protected static final Logger logger = LoggerFactory.getLogger("Message"); + public static Marker getMarker(String markerName) { + Marker m = MarkerFactory.getMarker(markerName); + return m; + } + public static void e(String a, String b){ + //android.util.Log.e(a, b); + logger.error(getMarker(a), b); + + //new com.eveningoutpost.dexdrip.Models.UserError(a, b); + } + + public static void e(String tag, String b, Exception e){ + //android.util.Log.e(tag, b, e); + logger.error(getMarker(tag), b, e); + + //new com.eveningoutpost.dexdrip.Models.UserError(tag, b + "\n" + e.toString()); + } + + public static void w(String tag, String b){ + //android.util.Log.w(tag, b); + logger.warn(getMarker(tag), b); + //com.eveningoutpost.dexdrip.Models.UserError.UserErrorLow(tag, b); + } + public static void w(String tag, String b, Exception e){ + //android.util.Log.w(tag, b, e); + logger.warn(getMarker(tag), b, e); + //com.eveningoutpost.dexdrip.Models.UserError.UserErrorLow(tag, b + "\n" + e.toString()); + } + public static void wtf(String tag, String b){ + //android.util.Log.wtf(tag, b); + logger.error(getMarker(tag), b); + //com.eveningoutpost.dexdrip.Models.UserError.UserErrorHigh(tag, b); + } + public static void wtf(String tag, String b, Exception e){ + //android.util.Log.wtf(tag, b, e); + logger.error(getMarker(tag), b, e); + //com.eveningoutpost.dexdrip.Models.UserError.UserErrorHigh(tag, b + "\n" + e.toString()); + } + public static void wtf(String tag, Exception e){ + //android.util.Log.wtf(tag, e); + logger.error(getMarker(tag), "Error", e); + //com.eveningoutpost.dexdrip.Models.UserError.UserErrorHigh(tag, e.toString()); + } + + public static void uel(String tag, String b) { + //android.util.Log.i(tag, b); + logger.info(getMarker(tag), b); + //com.eveningoutpost.dexdrip.Models.UserError.UserEventLow(tag, b); + } + + public static void ueh(String tag, String b) { + //android.util.Log.i(tag, b); + logger.info(getMarker(tag), b); + //com.eveningoutpost.dexdrip.Models.UserError.UserEventHigh(tag, b); + } + + public static void d(String tag, String b){ + //android.util.Log.d(tag, b); + logger.debug(getMarker(tag), b); + //if(com.eveningoutpost.dexdrip.Models.UserError.ExtraLogTags.shouldLogTag(tag, android.util.Log.DEBUG)) { + // UserErrorLow(tag, b); + //} + } + + public static void v(String tag, String b){ + //android.util.Log.v(tag, b); + logger.debug(getMarker(tag), b); + //if(com.eveningoutpost.dexdrip.Models.UserError.ExtraLogTags.shouldLogTag(tag, android.util.Log.VERBOSE)) { + // UserErrorLow(tag, b); + //} + } + + public static void i(String tag, String b){ + //android.util.Log.i(tag, b); + logger.info(getMarker(tag), b); + //if(com.eveningoutpost.dexdrip.Models.UserError.ExtraLogTags.shouldLogTag(tag, android.util.Log.INFO)) { + // UserErrorLow(tag, b); + //} + } + } + } +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoRxMessage.java new file mode 100644 index 0000000..d104722 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoRxMessage.java @@ -0,0 +1,53 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.UserError; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Created by jamorham on 25/11/2016. + */ + + +public class BatteryInfoRxMessage extends BaseMessage { + + private final static String TAG = G5CollectionService.TAG; // meh + + public static final byte opcode = 0x23; + + public int status; + public int voltagea; + public int voltageb; + public int resist; + public int runtime; + public int temperature; + + public BatteryInfoRxMessage(byte[] packet) { + if (packet.length >= 10) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if (data.get() == opcode) { + status = data.get(); + voltagea = getUnsignedShort(data); + voltageb = getUnsignedShort(data); + resist = getUnsignedShort(data); + runtime = getUnsignedByte(data); + if (packet.length == 10) { + runtime = -1; // this byte isn't runtime on rev2 + } + temperature = data.get(); // not sure if signed or not, but <0c or >127C seems unlikely! + } else { + UserError.Log.wtf(TAG, "Invalid opcode for BatteryInfoRxMessage"); + } + } else { + UserError.Log.wtf(TAG, "Invalid length for BatteryInfoMessage: " + packet.length); + } + } + + public String toString() { + return String.format(Locale.US, "Status: %s / VoltageA: %d / VoltageB: %d / Resistance: %d / Run Time: %d / Temperature: %d", + TransmitterStatus.getBatteryLevel(status).toString(), voltagea, voltageb, resist, runtime, temperature); + } + +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoTxMessage.java new file mode 100644 index 0000000..4b555d9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BatteryInfoTxMessage.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +/** + * Created by jamorham on 25/11/2016. + */ + +public class BatteryInfoTxMessage extends BaseMessage { + + private final static String TAG = G5CollectionService.TAG; // meh + static final byte opcode = 0x22; + + public BatteryInfoTxMessage() { + init(opcode, 3); + UserError.Log.e(TAG, "BatteryInfoTx dbg: " + JoH.bytesToHex(byteSequence)); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BluetoothServices.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BluetoothServices.java new file mode 100644 index 0000000..f43bd5f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BluetoothServices.java @@ -0,0 +1,63 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +import java.util.HashMap; +import java.util.UUID; + +/** + * Created by joeginley on 3/16/16. + */ +public class BluetoothServices { + + //Transmitter Service UUIDs + public static final UUID DeviceInfo = UUID.fromString("0000180A-0000-1000-8000-00805F9B34FB"); + //iOS uses FEBC? + public static final UUID Advertisement = UUID.fromString("0000FEBC-0000-1000-8000-00805F9B34FB"); + public static final UUID CGMService = UUID.fromString("F8083532-849E-531C-C594-30F1F86A4EA5"); + public static final UUID ServiceB = UUID.fromString("F8084532-849E-531C-C594-30F1F86A4EA5"); + + //DeviceInfoCharacteristicUUID, Read, DexcomUN + public static final UUID ManufacturerNameString = UUID.fromString("00002A29-0000-1000-8000-00805F9B34FB"); + + //CGMServiceCharacteristicUUID + public static final UUID Communication = UUID.fromString("F8083533-849E-531C-C594-30F1F86A4EA5"); + public static final UUID Control = UUID.fromString("F8083534-849E-531C-C594-30F1F86A4EA5"); + public static final UUID Authentication = UUID.fromString("F8083535-849E-531C-C594-30F1F86A4EA5"); + public static final UUID ProbablyBackfill = UUID.fromString("F8083536-849E-531C-C594-30F1F86A4EA5"); + + //ServiceBCharacteristicUUID + public static final UUID CharacteristicE = UUID.fromString("F8084533-849E-531C-C594-30F1F86A4EA5"); + public static final UUID CharacteristicF = UUID.fromString("F8084534-849E-531C-C594-30F1F86A4EA5"); + + //CharacteristicDescriptorUUID + public static final UUID CharacteristicUpdateNotification = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"); + + private static final HashMap mapToName = new HashMap<>(); + + + static { + mapToName.put(DeviceInfo, "DeviceInfo"); + mapToName.put(Advertisement, "Advertisement"); + mapToName.put(CGMService, "CGMService"); + mapToName.put(ServiceB, "ServiceB"); + mapToName.put(ManufacturerNameString, "ManufacturerNameString"); + mapToName.put(Communication, "Communication"); + mapToName.put(Control, "Control"); + mapToName.put(Authentication, "Authentication"); + mapToName.put(ProbablyBackfill, "ProbablyBackfill"); + mapToName.put(CharacteristicE, "CharacteristicE"); + mapToName.put(CharacteristicF, "CharacteristicF"); + mapToName.put(CharacteristicUpdateNotification, "CharacteristicUpdateNotification"); + } + + + public static String getUUIDName(UUID uuid) { + if (uuid == null) return "null"; + if (mapToName.containsKey(uuid)) { + return mapToName.get(uuid); + } else { + return "Unknown uuid: " + uuid.toString(); + } + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BondRequestTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BondRequestTxMessage.java new file mode 100644 index 0000000..34c735f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/BondRequestTxMessage.java @@ -0,0 +1,12 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +public class BondRequestTxMessage extends BaseMessage { + static final byte opcode = 0x07; + + public BondRequestTxMessage() { + init(opcode, 1); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CRC.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CRC.java new file mode 100644 index 0000000..b4eadfe --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CRC.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.CRC16; + +/** + * Created by jcostik1 on 3/24/16. + */ +public class CRC { + + public static byte[] calculate(byte b) { + int crcShort = 0; + crcShort = ((crcShort >>> 8) | (crcShort << 8)) & 0xffff; + crcShort ^= (b & 0xff); + crcShort ^= ((crcShort & 0xff) >> 4); + crcShort ^= (crcShort << 12) & 0xffff; + crcShort ^= ((crcShort & 0xFF) << 5) & 0xffff; + crcShort &= 0xffff; + return new byte[] {(byte) (crcShort & 0xff), (byte) ((crcShort >> 8) & 0xff)}; + } + + public static byte[] calculate(byte[] bytes, int start, int end) { + return CRC16.calculate(bytes, 0, end); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateRxMessage.java new file mode 100644 index 0000000..441d7f6 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateRxMessage.java @@ -0,0 +1,59 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// created by jamorham + +public class CalibrateRxMessage extends BaseMessage { + + public static final int opcode = 0x35; + private static final int length = 5; + + private byte info = (byte) 0xff; + private byte result = (byte) 0xff; + + CalibrateRxMessage(byte[] packet) { + + if (packet.length == length) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + info = data.get(); + result = data.get(); + } + } + } + + boolean accepted() { + return result == 0x00 || result == 0x06 || result == 0x0D; + } + + boolean wantsCalibration() { + return result == 0x06; + } + + String message() { + // TODO i18n + switch (result) { + + case (byte) 0x00: + return "OK"; + case (byte) 0x01: + return "Code 1"; + case (byte) 0x06: + return "Second calibration needed"; + case (byte) 0x08: + return "Rejected"; + case (byte) 0x0B: + return "Sensor stopped"; + case (byte) 0x0D: + return "Duplicate"; + case (byte) 0x0E: + return "Not ready to calibrate"; + case (byte) 0xFF: + return "Unable to decode"; + default: + return "Unknown code:" + result; + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateTxMessage.java new file mode 100644 index 0000000..f2abded --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrateTxMessage.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +// created by jamorham + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +public class CalibrateTxMessage extends BaseMessage { + + final byte opcode = 0x34; + final int length = 9; + + final int glucose; + + public CalibrateTxMessage(int glucose, int dexTime) { + init(opcode, length); + this.glucose = glucose; + data.putShort((short) glucose); + data.putInt(dexTime); + appendCRC(); + UserError.Log.d(TAG, "CalibrateGlucoseTxMessage dbg: " + JoH.bytesToHex(byteSequence)); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrationState.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrationState.java new file mode 100644 index 0000000..19f5160 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/CalibrationState.java @@ -0,0 +1,142 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// created by jamorham + +import android.util.SparseArray; + +import com.eveningoutpost.dexdrip.Models.UserError; +import com.google.common.collect.ImmutableSet; + +import lombok.Getter; + +import static com.eveningoutpost.dexdrip.Services.G5CollectionService.TAG; + +public enum CalibrationState { + + // TODO i18n + + Unknown(0x00, "Unknown"), + Stopped(0x01, "Stopped"), + WarmingUp(0x02, "Warming Up"), + ExcessNoise(0x03, "Excess Noise"), + NeedsFirstCalibration(0x04, "Needs Initial Calibration"), + NeedsSecondCalibration(0x05, "Needs Second Calibration"), + Ok(0x06, "OK"), + NeedsCalibration(0x07, "Needs Calibration"), + CalibrationConfused1(0x08, "Confused Calibration 1"), + CalibrationConfused2(0x09, "Confused Calibration 2"), + NeedsDifferentCalibration(0x0a, "Needs More Calibration"), + SensorFailed(0x0b, "Sensor Failed"), + SensorFailed2(0x0c, "Sensor Failed 2"), + UnusualCalibration(0x0d, "Unusual Calibration"), + InsufficientCalibration(0x0e, "Insufficient Calibration"), + Ended(0x0f, "Ended"), + SensorFailed3(0x10, "Sensor Failed 3"), + TransmitterProblem(0x11, "Transmitter Problem"), + Errors(0x12, "Sensor Errors"), + SensorFailed4(0x13, "Sensor Failed 4"), + SensorFailed5(0x14, "Sensor Failed 5"), + SensorFailed6(0x15, "Sensor Failed 6"), + SensorFailedStart(0x16, "Sensor Failed Start"); + + @Getter + byte value; + @Getter + String text; + + + private static final SparseArray lookup = new SparseArray<>(); + private static final ImmutableSet failed = ImmutableSet.of(SensorFailed, SensorFailed2, SensorFailed3, SensorFailed4, SensorFailed5, SensorFailed6, SensorFailedStart); + private static final ImmutableSet stopped = ImmutableSet.of(Stopped, Ended, SensorFailed, SensorFailed2, SensorFailed3, SensorFailed4, SensorFailed5, SensorFailed6, SensorFailedStart); + + + CalibrationState(int value, String text) { + this.value = (byte) value; + this.text = text; + } + + static { + for (CalibrationState state : values()) { + lookup.put(state.value, state); + } + } + + public static CalibrationState parse(byte state) { + final CalibrationState result = lookup.get(state); + if (result == null) UserError.Log.e(TAG, "Unknown calibration state: " + state); + return result != null ? result : Unknown; + } + + public static CalibrationState parse(int state) { + return parse((byte) state); + } + + public boolean usableGlucose() { + return this == Ok + || this == NeedsCalibration; + } + + public boolean insufficientCalibration() { + return this == InsufficientCalibration; + } + + public boolean readyForCalibration() { + return this == Ok + || needsCalibration(); + } + + public boolean needsCalibration() { + return this == NeedsCalibration + || this == NeedsFirstCalibration + || this == NeedsSecondCalibration + || this == NeedsDifferentCalibration; + } + + public boolean sensorStarted() { + return !stopped.contains(this); + } + + public boolean sensorFailed() { + return failed.contains(this); + } + + public boolean ended() { + return this == Ended; + } + + public boolean warmingUp() { + return this == WarmingUp; + } + + public boolean ok() { + return this == Ok; + } + + public boolean readyForBackfill() { + return this != WarmingUp && this != Stopped && this != Unknown && this != NeedsFirstCalibration && this != NeedsSecondCalibration && this != Errors; + } + + public String getExtendedText() { + switch (this) { + case Ok: + if (DexSessionKeeper.isStarted()) { + return getText() + " " + DexSessionKeeper.prettyTime(); + } else { + return getText() + " time?"; + } + case WarmingUp: + if (DexSessionKeeper.isStarted()) { + if (DexSessionKeeper.warmUpTimeValid()) { + return getText() + "\n" + DexSessionKeeper.prettyTime() + " left"; + } else { + return getText(); + } + } else { + return getText(); + } + + default: + return getText(); + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexResetHelper.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexResetHelper.java new file mode 100644 index 0000000..690a0d5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexResetHelper.java @@ -0,0 +1,40 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps; + +import static com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps.createActionIntent; +import static com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps.createChoiceIntent; +import static com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps.showNotification; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.DEX_BASE_ID; + +// jamorham + +public class DexResetHelper { + + private static final String TAG = "DexResetHelper"; + + public static void offer(String reason) { + + if (JoH.pratelimit("offer-hard-reset-dex", 1200)) { + int id = DEX_BASE_ID; + + final String title = "Hard Reset Transmitter?"; + + // piggybacking on the existing choice dialog system for compatible apps + showNotification(title, reason, + createActionIntent(id, id + 1, CompatibleApps.Feature.HARD_RESET_TRANSMITTER), + createActionIntent(id, id + 2, CompatibleApps.Feature.CANCEL), + createChoiceIntent(id, id + 3, CompatibleApps.Feature.HARD_RESET_TRANSMITTER, title, reason), + id); + } else { + UserError.Log.d(TAG, "Not offering reset as within rate limit"); + } + } + + public static void cancel() { + JoH.cancelNotification(DEX_BASE_ID); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSessionKeeper.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSessionKeeper.java new file mode 100644 index 0000000..ce30ec9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSessionKeeper.java @@ -0,0 +1,52 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; + +// jamorham + +// track active session time + +public class DexSessionKeeper { + + private static final String PREF_SESSION_START = "OB1-SESSION-START"; + private static final long WARMUP_PERIOD = Constants.HOUR_IN_MS * 2; + + public static void clearStart() { + PersistentStore.setLong(PREF_SESSION_START, 0); + } + + public static void setStart(long when) { + // TODO sanity check + PersistentStore.setLong(PREF_SESSION_START, when); + } + + public static long getStart() { + // value 0 == not started + return PersistentStore.getLong(PREF_SESSION_START); + } + + public static boolean isStarted() { + return getStart() != 0; + } + + public static String prettyTime() { + if (isStarted()) { + final long elapsed = JoH.msSince(getStart()); + if (elapsed < WARMUP_PERIOD) { + return JoH.niceTimeScalar((double) WARMUP_PERIOD - elapsed, 1); + } else { + return JoH.niceTimeScalar((double) elapsed, 1); + } + } else { + return ""; + } + } + + // check for mismatch between sensor state currency and transmitter time + public static boolean warmUpTimeValid() { + return (isStarted() && (JoH.msSince(getStart()) <= WARMUP_PERIOD)); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSyncKeeper.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSyncKeeper.java new file mode 100644 index 0000000..1c8e6d9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexSyncKeeper.java @@ -0,0 +1,84 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; + +import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD; + +public class DexSyncKeeper { + + private static final String TAG = DexTimeKeeper.class.getSimpleName(); + private static final String DEX_SYNC_STORE = "DEX_SYNC_STORE-"; + private static final long OLDEST_POSSIBLE = 1533839836123L; + private static final long GRACE_TIME = 5000; + private static final long VALIDITY_PERIOD = Constants.DAY_IN_MS; + + + // store sync time as now + public static void store(final String transmitterId) { + store(transmitterId, JoH.tsl()); + } + + // store sync time + public static void store(final String transmitterId, final long when) { + + if ((transmitterId == null) || (transmitterId.length() != 6)) { + UserError.Log.e(TAG, "Invalid dex transmitter in store: " + transmitterId); + return; + } + + if (when < OLDEST_POSSIBLE) { + UserError.Log.wtf(TAG, "Invalid timestamp to store: " + JoH.dateTimeText(when)); + return; + } + + PersistentStore.setLong(DEX_SYNC_STORE + transmitterId, when); + UserError.Log.d(TAG, "Sync time updated to: " + JoH.dateTimeText(when)); + } + + // anticpiate next wake up from now + public static long anticipate(final String transmitterId) { + return anticipate(transmitterId, JoH.tsl()); + } + + // anticipate next wake up from time + // -1 means we don't know anything + static long anticipate(final String transmitterId, final long now) { + final long last = PersistentStore.getLong(DEX_SYNC_STORE + transmitterId); + if (last < OLDEST_POSSIBLE) { + return -1; + } + if (last > now) { + UserError.Log.e(TAG, "Anticipation time in the future! cannot use: " + JoH.dateTimeText(last)); + return -1; // can't be in the future + } + + if (now - last > VALIDITY_PERIOD) { + UserError.Log.e(TAG, "Anticipation time too old to use: " + JoH.dateTimeText(last)); + return -1; + } + + final long modulo = (now - last) % DEXCOM_PERIOD; + if ((modulo < GRACE_TIME) && ((now - last) > GRACE_TIME)) return now; + final long next = now + (DEXCOM_PERIOD - modulo); + return next; + } + + public static boolean isReady(final String transmitterId) { + return anticipate(transmitterId, JoH.tsl()) != -1; + } + + // are we outside connection window? + // TODO also handle waking up before window PRE_GRACE_TIME = 20seconds? + public static boolean outsideWindow(final String transmitterId) { + final long now = JoH.tsl(); + final long next = anticipate(transmitterId, now); + return next != now; + } + +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexTimeKeeper.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexTimeKeeper.java new file mode 100644 index 0000000..64c48be --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DexTimeKeeper.java @@ -0,0 +1,140 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; + +/** + * Created by jamorham on 25/11/2016. + */ + +public class DexTimeKeeper { + private static final String TAG = DexTimeKeeper.class.getSimpleName(); + + private static final String DEX_XMIT_START = "DEX_XMIT_START-"; + private static final long OLDEST_ALLOWED = 1512245359123L; + private static final long DEX_TRANSMITTER_LIFE_SECONDS = 86400 * 120; + + private static String lastTransmitterId = null; + + // update the activation time stored for a transmitter + public static void updateAge(final String transmitterId, final int dexTimeStamp) { + updateAge(transmitterId, dexTimeStamp, false); + } + + // update the activation time stored for a transmitter + public static void updateAge(final String transmitterId, final int dexTimeStamp, final boolean absolute) { + + if ((transmitterId == null) || (transmitterId.length() != 6)) { + UserError.Log.e(TAG, "Invalid dex transmitter in updateAge: " + transmitterId); + return; + } + if (dexTimeStamp < 1) { + UserError.Log.e(TAG, "Invalid dex timestamp in updateAge: " + dexTimeStamp); + if (dexTimeStamp == 0 && absolute) { + DexResetHelper.offer("Your transmitter clock has stopped or never started. Do you want to hard reset it?"); + } + return; + } + final long longDexTimeStamp = (long) dexTimeStamp; + final long activation_time = JoH.tsl() - (longDexTimeStamp * 1000L); + + if (activation_time > JoH.tsl()) { + UserError.Log.wtf(TAG, "Transmitter activation time is in the future. Not possible to update: " + dexTimeStamp); + return; + } + + UserError.Log.d(TAG, "Activation time updated to: " + JoH.dateTimeText(activation_time)); + PersistentStore.setLong(DEX_XMIT_START + transmitterId, activation_time); + + } + + public static int getDexTime(String transmitterId, long timestamp) { + + if ((transmitterId == null) || (transmitterId.length() != 6)) { + UserError.Log.e(TAG, "Invalid dex transmitter in getDexTime: " + transmitterId); + return -3; + } + if (timestamp < OLDEST_ALLOWED) { + UserError.Log.e(TAG, "Invalid timestamp in getDexTime: " + timestamp); + return -2; + } + + final long transmitter_start_timestamp = PersistentStore.getLong(DEX_XMIT_START + transmitterId); + + if (transmitter_start_timestamp < OLDEST_ALLOWED) { + if (JoH.ratelimit("no-valid-dex-timestamp-log", 60)) { + UserError.Log.e(TAG, "No valid timestamp stored for transmitter: " + transmitterId); + } + return -1; + } + + final long ms_since = timestamp - transmitter_start_timestamp; + if (ms_since < 0) { + UserError.Log.e(TAG, "Invalid timestamp comparison for transmitter id: " + transmitterId + " since: " + ms_since + " requested ts: " + JoH.dateTimeText(timestamp) + " with tx start: " + JoH.dateTimeText(transmitter_start_timestamp)); + return -4; + } + lastTransmitterId = transmitterId; + return (int) (ms_since / 1000L); + } + + public static long fromDexTimeCached(int dexTimeStamp) { + return fromDexTime(lastTransmitterId, dexTimeStamp); + } + + + public static long fromDexTime(String transmitterId, int dexTimeStamp) { + if ((transmitterId == null) || (transmitterId.length() != 6)) { + UserError.Log.e(TAG, "Invalid dex transmitter in fromDexTime: " + transmitterId); + return -3; + } + lastTransmitterId = transmitterId; + final long transmitter_start_timestamp = PersistentStore.getLong(DEX_XMIT_START + transmitterId); + if (transmitter_start_timestamp > 0) { + return transmitter_start_timestamp + (((long) dexTimeStamp) * 1000L); + } else { + return -1; + } + + } + + // should we try to use this transmitter + public static boolean isInDate(String transmitterId) { + final int valid_time = getDexTime(transmitterId, JoH.tsl()); + return (valid_time >= 0) && (valid_time < DEX_TRANSMITTER_LIFE_SECONDS); + } + + public static int getTransmitterAgeInDays(final String transmitterId) { + final int valid_time = getDexTime(transmitterId, JoH.tsl()); + return (valid_time >= 0) ? valid_time / 86400 : -1; + } + + public static String extractForStream(String transmitterId) { + if (transmitterId == null || transmitterId.length() == 0) return null; + final long result = PersistentStore.getLong(DEX_XMIT_START + transmitterId); + if (result == 0) return null; + return transmitterId + "^" + result; + } + + public static void injectFromStream(String stream) { + if (stream == null) return; + final String[] components = stream.split("\\^"); + try { + if (components.length == 2) { + final long time_stamp = Long.parseLong(components[1]); + if (time_stamp > OLDEST_ALLOWED) { + PersistentStore.setLong(DEX_XMIT_START + components[0], time_stamp); + UserError.Log.d(TAG, "Updating time keeper: " + components[0] + " " + JoH.dateTimeText(time_stamp)); + } else { + UserError.Log.wtf(TAG, "Dex Timestamp doesn't meet criteria: " + time_stamp); + } + } else { + UserError.Log.e(TAG, "Invalid injectFromStream length: " + stream); + } + } catch (NumberFormatException e) { + UserError.Log.e(TAG, "Invalid injectFromStream: " + stream + " " + e); + } + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DisconnectTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DisconnectTxMessage.java new file mode 100644 index 0000000..6be3e98 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/DisconnectTxMessage.java @@ -0,0 +1,21 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; + +/** + * Created by joeginley on 3/16/16. + */ +public class DisconnectTxMessage extends BaseMessage { + byte opcode = 0x09; + private final static String TAG = G5CollectionService.TAG; // meh + public DisconnectTxMessage() { + data = ByteBuffer.allocate(1); + data.put(opcode); + + byteSequence = data.array(); + UserError.Log.d(TAG,"DisconnectTX: "+ JoH.bytesToHex(byteSequence)); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseRxMessage.java new file mode 100644 index 0000000..cf480f5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseRxMessage.java @@ -0,0 +1,60 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class EGlucoseRxMessage extends BaseGlucoseRxMessage { + + private static final String TAG = EGlucoseRxMessage.class.getSimpleName(); + + private Integer predicted_glucose; // : UInt16 + public static final byte opcode = 0x4f; + + public EGlucoseRxMessage(byte[] packet) { + UserError.Log.d(TAG, "EGlucoseRX dbg: " + JoH.bytesToHex(packet)); + if (packet.length >= 14) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + + + //status_raw = data.get(); + status = TransmitterStatus.getBatteryLevel(data.get()); // ?? + sequence = data.getInt(); + timestamp = data.getInt(); + + + int glucoseBytes = data.getShort(); // check signed vs unsigned!! + glucoseIsDisplayOnly = (glucoseBytes & 0xf000) > 0; + glucose = glucoseBytes & 0xfff; + + state = data.get(); + trend = data.get(); + + if (glucose > 13) { + unfiltered = glucose * 1000; + filtered = glucose * 1000; + } else { + filtered = glucose; + unfiltered = glucose; + } + + predicted_glucose = data.getShort() & 0x03ff; // needs mask??? // remaining bits?? + + UserError.Log.d(TAG, "EGlucoseRX: seq:" + sequence + " ts:" + timestamp + " sg:" + glucose + " psg: " + predicted_glucose + " do:" + glucoseIsDisplayOnly + " ss:" + status + " sr:" + status_raw + " st:" + CalibrationState.parse(state) + " tr:" + getTrend()); + + } + } else { + UserError.Log.d(TAG, "GlucoseRxMessage packet length received wrong: " + packet.length); + } + + } + + @Override + public Integer getPredictedGlucose() { + return predicted_glucose; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseTxMessage.java new file mode 100644 index 0000000..21ea09f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/EGlucoseTxMessage.java @@ -0,0 +1,19 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// created by jamorham + + +public class EGlucoseTxMessage extends BaseMessage { + + final byte opcode = 0x4e; + + public EGlucoseTxMessage() { + data = ByteBuffer.allocate(3).order(ByteOrder.LITTLE_ENDIAN); + data.put(opcode); + appendCRC(); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Extensions.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Extensions.java new file mode 100644 index 0000000..a5cfddc --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Extensions.java @@ -0,0 +1,39 @@ +package com.eveningoutpost.dexdrip.G5Model; + +/** + * Created by joeginley on 3/19/16. + */ +public class Extensions { + + public static String bytesToHex(byte[] in) { + final StringBuilder builder = new StringBuilder(); + for(byte b : in) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + + public static byte[] hexToBytes(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static String lastTwoCharactersOfString(final String s) { + if (s == null) return "NULL"; + return s.length() > 1 ? s.substring(s.length() - 2) : "ERR-" + s; + } + + public static void doSleep(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/F2DUnknownRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/F2DUnknownRxMessage.java new file mode 100644 index 0000000..3d6b6e2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/F2DUnknownRxMessage.java @@ -0,0 +1,12 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +public class F2DUnknownRxMessage extends BaseMessage { + + public static final byte opcode = (byte) 0xD0; + + public F2DUnknownRxMessage(final byte[] packet) { + // stub + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FastCRC16.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FastCRC16.java new file mode 100644 index 0000000..ff5193f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FastCRC16.java @@ -0,0 +1,26 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +public class FastCRC16 { + + private static final int table[] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 0x8616}; + + public static byte[] calculate(final byte b) { + final int crc = table[b & 0xff]; + return new byte[]{(byte) (crc & 0xff), (byte) ((crc >> 8) & 0xff)}; + } + + public static byte[] calculate(final byte[] bytes, int end) { + int crc = 0; + for (int i = 0; i < end; i++) { + crc = (crc << 8) ^ table[(crc >>> 8 ^ bytes[i]) & 0xff]; + } + return new byte[]{(byte) (crc & 0xff), (byte) ((crc >> 8) & 0xff)}; + } + + public static byte[] calculate(final byte[] bytes) { + return calculate(bytes, bytes.length); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FirmwareCapability.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FirmwareCapability.java new file mode 100644 index 0000000..81e44ac --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/FirmwareCapability.java @@ -0,0 +1,100 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.google.common.collect.ImmutableSet; + +import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.getRawFirmwareVersionString; + +public class FirmwareCapability { + + private static final ImmutableSet KNOWN_G5_FIRMWARES = ImmutableSet.of("1.0.0.13", "1.0.0.17", "1.0.4.10", "1.0.4.12"); + private static final ImmutableSet KNOWN_G6_FIRMWARES = ImmutableSet.of("1.6.5.23", "1.6.5.25", "1.6.5.27"); + private static final ImmutableSet KNOWN_G6_REV2_FIRMWARES = ImmutableSet.of("2.18.2.67", "2.18.2.88", "2.18.2.98"); + private static final ImmutableSet KNOWN_G6_REV2_RAW_FIRMWARES = ImmutableSet.of("2.18.2.67"); + private static final ImmutableSet KNOWN_G6_PLUS_FIRMWARES = ImmutableSet.of("2.4.2.88"); + private static final ImmutableSet KNOWN_TIME_TRAVEL_TESTED = ImmutableSet.of("1.6.5.25"); + + // new G6 firmware versions will need to be added here / above + static boolean isG6Firmware(final String version) { + return version != null && (KNOWN_G6_FIRMWARES.contains(version) + || KNOWN_G6_REV2_FIRMWARES.contains(version) + || KNOWN_G6_PLUS_FIRMWARES.contains(version) + || version.startsWith("1.6.5.") + || version.startsWith("2.18.") + || version.startsWith("2.4.")); + } + + public static boolean isG6Rev2(final String version) { + return version != null && (KNOWN_G6_REV2_FIRMWARES.contains(version) || version.startsWith("2.18.")); + } + + public static boolean isG6Plus(final String version) { + return version != null && (KNOWN_G6_PLUS_FIRMWARES.contains(version) || version.startsWith("2.4.")); + } + + static boolean isG5Firmware(final String version) { + return KNOWN_G5_FIRMWARES.contains(version); + } + + static boolean isFirmwareTimeTravelCapable(final String version) { + return KNOWN_TIME_TRAVEL_TESTED.contains(version); + } + + public static boolean isFirmwareTemperatureCapable(final String version) { + return !isG6Rev2(version) && !isG6Plus(version); + } + + private static boolean isFirmwarePredictiveCapable(final String version) { + return isG6Firmware(version); + } + + static boolean isFirmwareRawCapable(final String version) { + return version == null + || version.equals("") + || KNOWN_G5_FIRMWARES.contains(version) + || KNOWN_G6_FIRMWARES.contains(version) + || KNOWN_G6_REV2_RAW_FIRMWARES.contains(version); + } + + static boolean isFirmwarePreemptiveRestartCapable(final String version) { + return isFirmwareRawCapable(version); // hang off this for now as they are currently the same + } + + public static boolean isTransmitterPredictiveCapable(final String tx_id) { + return isG6Firmware(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterG5(final String tx_id) { + return isG5Firmware(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterG6(final String tx_id) { + return isG6Firmware(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterG6Rev2(final String tx_id) { + return isG6Rev2(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterTimeTravelCapable(final String tx_id) { + return isFirmwareTimeTravelCapable(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterRawCapable(final String tx_id) { + return isFirmwareRawCapable(getRawFirmwareVersionString(tx_id)); + } + + public static boolean isTransmitterPreemptiveRestartCapable(final String tx_id) { + return isFirmwarePreemptiveRestartCapable(getRawFirmwareVersionString(tx_id)); + } + + static long getWarmupPeriodForVersion(final String version) { + return isG6Plus(version) ? Constants.HOUR_IN_MS : Constants.HOUR_IN_MS * 2; + } + + public static long getWarmupPeriod(final String tx_id) { + return getWarmupPeriod(getRawFirmwareVersionString(tx_id)); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/G6CalibrationParameters.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/G6CalibrationParameters.java new file mode 100644 index 0000000..5bf565e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/G6CalibrationParameters.java @@ -0,0 +1,140 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; + +import lombok.Getter; + +public class G6CalibrationParameters { + + public static final String PREF_CURRENT_CODE = "G6-Current-Sensor-Code"; + + @Getter + private final String code; + @Getter + private final int paramA; + @Getter + private final int paramB; + + + public G6CalibrationParameters(String code) { + + this.code = code; + + switch (code) { + + // special null code + case "0000": + paramA = 1; + paramB = 0; + break; + + case "5915": + paramA = 3100; + paramB = 3600; + break; + + case "5917": + paramA = 3000; + paramB = 3500; + break; + + case "5931": + paramA = 2900; + paramB = 3400; + break; + + case "5937": + paramA = 2800; + paramB = 3300; + break; + + case "5951": + paramA = 3100; + paramB = 3500; + break; + + case "5955": + paramA = 3000; + paramB = 3400; + break; + + case "7171": + paramA = 2700; + paramB = 3300; + break; + + case "9117": + paramA = 2700; + paramB = 3200; + break; + + case "9159": + paramA = 2600; + paramB = 3200; + break; + + case "9311": + paramA = 2600; + paramB = 3100; + break; + + case "9371": + paramA = 2500; + paramB = 3100; + break; + + case "9515": + paramA = 2500; + paramB = 3000; + break; + + case "9551": + paramA = 2400; + paramB = 3000; + break; + + case "9577": + paramA = 2400; + paramB = 2900; + break; + + case "9713": + paramA = 2300; + paramB = 2900; + break; + + default: + paramA = -1; + paramB = -1; + } + + } + + public boolean isValid() { + return paramA > 0; + } + + public boolean isNullCode() { + return isValid() && paramB == 0; + } + + public static boolean checkCode(String code) { + return new G6CalibrationParameters(code).isValid(); + } + + public static String getCurrentSensorCode() { + final String code = PersistentStore.getString(PREF_CURRENT_CODE); + return code.equals("") ? null : code; + } + + public static void setCurrentSensorCode(String code) { + if (checkCode(code)) { + PersistentStore.setString(PREF_CURRENT_CODE, code); + } else { + PersistentStore.setString(PREF_CURRENT_CODE, ""); + throw new RuntimeException("Invalid sensor code: " + code); + } + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseRxMessage.java new file mode 100644 index 0000000..e904786 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseRxMessage.java @@ -0,0 +1,64 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import lombok.NoArgsConstructor; + +/** + * Created by jamorham on 25/11/2016. + * + * Alternate mechanism for reading data using the transmitter's internal algorithm. + * + * initial packet structure cribbed from Loopkit + */ + +@NoArgsConstructor +public class GlucoseRxMessage extends BaseGlucoseRxMessage { + + private final static String TAG = G5CollectionService.TAG; // meh + + public static final byte opcode = 0x31; + + + public GlucoseRxMessage(byte[] packet) { + UserError.Log.d(TAG, "GlucoseRX dbg: " + JoH.bytesToHex(packet)); + if (packet.length >= 14) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + + status_raw = data.get(1); + status = TransmitterStatus.getBatteryLevel(data.get(1)); + sequence = data.getInt(2); + timestamp = data.getInt(6); + + + int glucoseBytes = data.getShort(10); // check signed vs unsigned!! + glucoseIsDisplayOnly = (glucoseBytes & 0xf000) > 0; + glucose = glucoseBytes & 0xfff; + + state = data.get(12); + trend = data.get(13); + if (glucose > 13) { + unfiltered = glucose * 1000; + filtered = glucose * 1000; + } else { + filtered = glucose; + unfiltered = glucose; + } + + UserError.Log.d(TAG, "GlucoseRX: seq:" + sequence + " ts:" + timestamp + " sg:" + glucose + " do:" + glucoseIsDisplayOnly + " ss:" + status + " sr:" + status_raw + " st:" + CalibrationState.parse(state) + " tr:" + getTrend()); + + } + } else { + UserError.Log.d(TAG, "GlucoseRxMessage packet length received wrong: " + packet.length); + } + + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseTxMessage.java new file mode 100644 index 0000000..42ee166 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/GlucoseTxMessage.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + + +/** + * Created by jamorham on 25/11/2016. + */ + +public class GlucoseTxMessage extends BaseMessage { + + private final static String TAG = G5CollectionService.TAG; // meh + static final byte opcode = 0x30; + + public GlucoseTxMessage() { + init(opcode, 3); + UserError.Log.d(TAG, "GlucoseTx dbg: " + JoH.bytesToHex(byteSequence)); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/InvalidRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/InvalidRxMessage.java new file mode 100644 index 0000000..70de647 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/InvalidRxMessage.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class InvalidRxMessage extends BaseMessage { + + public static final byte opcode = (byte) 0xFF; + private static final int length = 3; + + InvalidRxMessage(byte[] packet) { + + if ((packet.length == length) && packet[0] == opcode) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + } + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/KeepAliveTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/KeepAliveTxMessage.java new file mode 100644 index 0000000..f56f30d --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/KeepAliveTxMessage.java @@ -0,0 +1,29 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.Services.G5CollectionService; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by joeginley on 3/16/16. + */ +public class KeepAliveTxMessage extends BaseMessage { + public static final int opcode = 0x06; + private int time; + + private final static String TAG = G5CollectionService.TAG; // meh + + public KeepAliveTxMessage(int time) { + this.time = time; + + data = ByteBuffer.allocate(2); + data.put(new byte[]{(byte) opcode, (byte) this.time}); + byteSequence = data.order(ByteOrder.LITTLE_ENDIAN).array(); + + UserError.Log.d(TAG, "New KeepAliveRequestTxMessage: " + JoH.bytesToHex(byteSequence)); + + } +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1G5StateMachine.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1G5StateMachine.java new file mode 100644 index 0000000..aaad9d5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1G5StateMachine.java @@ -0,0 +1,1686 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothGatt; +import android.os.Build; +import android.os.PowerManager; + +import com.eveningoutpost.dexdrip.BestGlucose; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; +import com.eveningoutpost.dexdrip.Models.BgReading; +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.Prediction; +import com.eveningoutpost.dexdrip.Models.Sensor; +import com.eveningoutpost.dexdrip.Models.SensorSanity; +import com.eveningoutpost.dexdrip.Models.TransmitterData; +import com.eveningoutpost.dexdrip.Models.Treatments; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService; +import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder; +import com.eveningoutpost.dexdrip.UtilityModels.BroadcastGlucose; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; +import com.eveningoutpost.dexdrip.UtilityModels.NotificationChannels; +import com.eveningoutpost.dexdrip.UtilityModels.Notifications; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.eveningoutpost.dexdrip.utils.PowerStateReceiver; +import com.eveningoutpost.dexdrip.utils.bt.Mimeograph; +import com.eveningoutpost.dexdrip.wearintegration.WatchUpdaterService; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.reflect.TypeToken; +/*import com.polidea.rxandroidble.RxBleConnection; +import com.polidea.rxandroidble.exceptions.BleCannotSetCharacteristicNotificationException; +import com.polidea.rxandroidble.exceptions.BleDisconnectedException; +import com.polidea.rxandroidble.exceptions.BleGattCharacteristicException; +*/ +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +//import rx.schedulers.Schedulers; + +import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.Authentication; +import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.Control; +import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.ProbablyBackfill; +import static com.eveningoutpost.dexdrip.Models.JoH.msSince; +import static com.eveningoutpost.dexdrip.Models.JoH.pratelimit; +import static com.eveningoutpost.dexdrip.Models.JoH.tsl; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.G5_BATTERY_FROM_MARKER; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.G5_BATTERY_LEVEL_MARKER; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.G5_BATTERY_MARKER; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.G5_BATTERY_WEARABLE_SEND; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.G5_FIRMWARE_MARKER; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.android_wear; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.getTransmitterID; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.onlyUsingNativeMode; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.wear_broadcast; +import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.DAY_IN_MS; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.HOUR_IN_MS; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.MINUTE_IN_MS; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.SECOND_IN_MS; +import static com.eveningoutpost.dexdrip.utils.bt.Helper.getStatusName; + + +import com.polidea.rxandroidble2.RxBleConnection; +import com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException; +import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; +import com.polidea.rxandroidble2.exceptions.BleGattCharacteristicException; + +import io.reactivex.schedulers.Schedulers; + + +/** + * Created by jamorham on 17/09/2017. + *

+ * Handles OB1 G5 communication logic + */ + +@SuppressWarnings("AccessStaticViaInstance") +public class Ob1G5StateMachine { + + private static final String TAG = "Ob1G5StateMachine"; + private static final String PREF_SAVED_QUEUE = "Ob1-saved-queue"; + + public static final String PREF_QUEUE_DRAINED = "OB1-QUEUE-DRAINED"; + public static final String CLOSED_OK_TEXT = "Closed OK"; + + private static final int LOW_BATTERY_WARNING_LEVEL = Pref.getStringToInt("g5-battery-warning-level", 300); // voltage a < this value raises warnings; + private static final long BATTERY_READ_PERIOD_MS = HOUR_IN_MS * 12; // how often to poll battery data (12 hours) + private static final long MAX_BACKFILL_PERIOD_MS = HOUR_IN_MS * 3; // how far back to request backfill data + private static final int BACKFILL_CHECK_SMALL = 3; + private static final int BACKFILL_CHECK_LARGE = (int) (MAX_BACKFILL_PERIOD_MS / DEXCOM_PERIOD); + + private static final boolean getVersionDetails = true; // try to load firmware version details + private static final boolean getBatteryDetails = true; // try to load battery info details + + private static final LinkedBlockingDeque commandQueue = new LinkedBlockingDeque<>(); + + private static boolean speakSlowly = false; // slow down bluetooth comms for android wear etc + private static int nextBackFillCheckSize = BACKFILL_CHECK_SMALL; + + private static final boolean d = false; + + private static volatile long lastGlucosePacket = 0; + private static volatile long lastUsableGlucosePacket = 0; + private static volatile BgReading lastGlucoseBgReading; + private static volatile boolean backup_loaded = false; + + // Auth Check + Request + @SuppressLint("CheckResult") + public static boolean doCheckAuth(Ob1G5CollectionService parent, RxBleConnection connection) { + + if (connection == null) return false; + parent.msg("Authorizing"); + + if (parent.android_wear) { + speakSlowly = true; + UserError.Log.d(TAG, "Setting speak slowly to true"); // WARN should be reactive or on named devices + } + + final AuthRequestTxMessage authRequest = new AuthRequestTxMessage(getTokenSize(), usingAlt()); + UserError.Log.i(TAG, "AuthRequestTX: " + JoH.bytesToHex(authRequest.byteSequence)); + + connection.setupNotification(Authentication) + // .timeout(10, TimeUnit.SECONDS) + .timeout(15, TimeUnit.SECONDS) // WARN + // .observeOn(Schedulers.newThread()) // needed? + .doOnNext(notificationObservable -> { + connection.writeCharacteristic(Authentication, nn(authRequest.byteSequence)) + .subscribe( + characteristicValue -> { + // Characteristic value confirmed. + if (d) + UserError.Log.d(TAG, "Wrote authrequest, got: " + JoH.bytesToHex(characteristicValue)); + speakSlowly(); + connection.readCharacteristic(Authentication).subscribe( + readValue -> { + authenticationProcessor(parent, connection, readValue); + }, throwable -> { + UserError.Log.e(TAG, "Could not read after AuthRequestTX: " + throwable); + }); + //parent.background_automata(); + }, + throwable -> { + UserError.Log.e(TAG, "Could not write AuthRequestTX: " + throwable); + parent.incrementErrors(); + } + + ); + }).flatMap(notificationObservable -> notificationObservable) + //.timeout(5, TimeUnit.SECONDS) + //.observeOn(Schedulers.newThread()) + .subscribe(bytes -> { + // incoming notifications + UserError.Log.d(TAG, "Received Authentication notification bytes: " + JoH.bytesToHex(bytes)); + authenticationProcessor(parent, connection, bytes); + + }, throwable -> { + if (!(throwable instanceof OperationSuccess)) { + if (((parent.getState() == Ob1G5CollectionService.STATE.CLOSED) + || (parent.getState() == Ob1G5CollectionService.STATE.CLOSE)) + && (throwable instanceof BleDisconnectedException)) { + UserError.Log.d(TAG, "normal authentication notification throwable: (" + parent.getState() + ") " + throwable + " " + JoH.dateTimeText(tsl())); + parent.connectionStateChange(CLOSED_OK_TEXT); + } else if ((parent.getState() == Ob1G5CollectionService.STATE.BOND) && (throwable instanceof TimeoutException)) { + // TODO Trigger on Error count / Android wear metric + // UserError.Log.e(TAG,"Attempting to reset/create bond due to: "+throwable); + // parent.reset_bond(true); + // parent.unBond(); // WARN + } else { + UserError.Log.e(TAG, "authentication notification throwable: (" + parent.getState() + ") " + throwable + " " + JoH.dateTimeText(tsl())); + parent.incrementErrors(); + if (throwable instanceof BleCannotSetCharacteristicNotificationException + || throwable instanceof BleGattCharacteristicException) { + parent.tryGattRefresh(); + parent.changeState(Ob1G5CollectionService.STATE.SCAN); + } + } + if ((throwable instanceof BleDisconnectedException) || (throwable instanceof TimeoutException)) { + if ((parent.getState() == Ob1G5CollectionService.STATE.BOND) || (parent.getState() == Ob1G5CollectionService.STATE.CHECK_AUTH)) { + + if (parent.getState() == Ob1G5CollectionService.STATE.BOND) { + UserError.Log.d(TAG, "SLEEPING BEFORE RECONNECT"); + threadSleep(15000); + } + UserError.Log.d(TAG, "REQUESTING RECONNECT"); + parent.changeState(Ob1G5CollectionService.STATE.SCAN); + } + } + } + }); + return true; + } + + @SuppressLint("CheckResult") + private static void authenticationProcessor(final Ob1G5CollectionService parent, final RxBleConnection connection, final byte[] readValue) { + PacketShop pkt = classifyPacket(readValue); + UserError.Log.d(TAG, "Read from auth request: " + pkt.type + " " + JoH.bytesToHex(readValue)); + + switch (pkt.type) { + case AuthChallengeRxMessage: + // Respond to the challenge request + byte[] challengeHash = calculateChallengeHash(((AuthChallengeRxMessage) pkt.msg).challenge); + if (d) + UserError.Log.d(TAG, "challenge hash" + Arrays.toString(challengeHash)); + if (challengeHash != null) { + if (d) + UserError.Log.d(TAG, "Transmitter trying auth challenge"); + + connection.writeCharacteristic(Authentication, nn(new BaseAuthChallengeTxMessage(challengeHash).byteSequence)) + .subscribe( + challenge_value -> { + + speakSlowly(); + + connection.readCharacteristic(Authentication) + //.observeOn(Schedulers.io()) + .subscribe( + status_value -> { + // interpret authentication response + authenticationProcessor(parent, connection, status_value); + }, throwable -> { + if (throwable instanceof OperationSuccess) { + UserError.Log.d(TAG, "Stopping auth challenge listener due to success"); + } else { + UserError.Log.e(TAG, "Could not read reply to auth challenge: " + throwable); + parent.incrementErrors(); + speakSlowly = true; + } + }); + }, throwable -> { + UserError.Log.e(TAG, "Could not write auth challenge reply: " + throwable); + parent.incrementErrors(); + }); + + } else { + UserError.Log.e(TAG, "Could not generate challenge hash! - resetting"); + parent.changeState(Ob1G5CollectionService.STATE.INIT); + parent.incrementErrors(); + return; + } + + break; + + case AuthStatusRxMessage: + final AuthStatusRxMessage status = (AuthStatusRxMessage) pkt.msg; + if (d) + UserError.Log.d(TAG, ("Authenticated: " + status.isAuthenticated() + " " + status.isBonded())); + if (status.isAuthenticated()) { + if (status.isBonded()) { + parent.msg("Authenticated"); + parent.authResult(true); + parent.changeState(Ob1G5CollectionService.STATE.GET_DATA); + throw new OperationSuccess("Authenticated"); + } else { + //parent.unBond(); // bond must be invalid or not existing // WARN + parent.changeState(Ob1G5CollectionService.STATE.PREBOND); + // TODO what to do here? + } + } else { + parent.msg("Not Authorized! (Wrong TxID?)"); + UserError.Log.e(TAG, "Authentication failed!!!!"); + parent.incrementErrors(); + // TODO? try again? + } + break; + + case BondRequestRxMessage: + UserError.Log.d(TAG, "Wrote bond request successfully"); + parent.waitingBondConfirmation = 1; // waiting + + parent.instantCreateBondIfAllowed(); + UserError.Log.d(TAG, "Sleeping for bond"); + for (int i = 0; i < 9; i++) { + if (parent.waitingBondConfirmation == 2) { + UserError.Log.d(TAG, "Bond confirmation received - continuing!"); + break; + } + threadSleep(1000); + } + parent.changeState(Ob1G5CollectionService.STATE.BOND); + break; + + default: + UserError.Log.e(TAG, "Unhandled packet type in reply: " + pkt.type + " " + JoH.bytesToHex(readValue)); + parent.incrementErrors(); + // TODO what to do here? + break; + } + } + + private static final int SPEAK_SLOWLY_DELAY = 300; + + private static int speakSlowlyDelay() { + return speakSlowly ? SPEAK_SLOWLY_DELAY : 0; + } + + private static void speakSlowly() { + if (speakSlowly) { + UserError.Log.d(TAG, "Speaking slowly"); + threadSleep(SPEAK_SLOWLY_DELAY); + } + } + + private static void threadSleep(int ms) { + try { + Thread.sleep(ms); + } catch (Exception e) { + UserError.Log.e(TAG, "Failed to sleep for " + ms + " due to: " + e); + } + } + + + @SuppressLint("CheckResult") + public synchronized static void doKeepAlive(Ob1G5CollectionService parent, RxBleConnection connection, Runnable runnable) { + if (connection == null) return; + connection.writeCharacteristic(Authentication, nn(new KeepAliveTxMessage(60).byteSequence)) + .timeout(2, TimeUnit.SECONDS) + .subscribe( + characteristicValue -> { + UserError.Log.d(TAG, "Sent keep-alive " + ((runnable != null) ? "Running runnable chain" : "")); + if (runnable != null) { + runnable.run(); + } + throw new OperationSuccess("keep-alive runnable complete"); + }, throwable -> { + if (!(throwable instanceof OperationSuccess)) { + UserError.Log.e(TAG, "Got error sending keepalive: " + throwable); + } + }); + } + + // Handle bonding + @SuppressLint("CheckResult") + public synchronized static boolean doKeepAliveAndBondRequest(Ob1G5CollectionService parent, RxBleConnection connection) { + + if (connection == null) return false; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + UserError.Log.d(TAG, "Requesting high priority"); + connection.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH, 500, TimeUnit.MILLISECONDS); + } + UserError.Log.e(TAG, "Sending keepalive.."); + connection.writeCharacteristic(Authentication, nn(new KeepAliveTxMessage(60).byteSequence)) + .subscribe( + characteristicValue -> { + UserError.Log.d(TAG, "Wrote keep-alive request successfully"); + speakSlowly(); // is this really needed here? + parent.unBond(); + parent.instantCreateBondIfAllowed(); + speakSlowly(); + connection.writeCharacteristic(Authentication, nn(new BondRequestTxMessage().byteSequence)) + .subscribe( + bondRequestValue -> { + UserError.Log.d(TAG, "Wrote bond request value: " + JoH.bytesToHex(bondRequestValue)); + speakSlowly(); + connection.readCharacteristic(Authentication) + .observeOn(Schedulers.io()) + .timeout(10, TimeUnit.SECONDS) + .subscribe( + status_value -> { + UserError.Log.d(TAG, "Got status read after keepalive " + JoH.bytesToHex(status_value)); + authenticationProcessor(parent, connection, status_value); + throw new OperationSuccess("Bond requested"); + }, throwable -> { + UserError.Log.e(TAG, "Throwable when reading characteristic after keepalive: " + throwable); + }); + + // Wrote bond request successfully was here moved above - is this right? + }, throwable -> { + // failed to write bond request retry? + if (!(throwable instanceof OperationSuccess)) { + UserError.Log.e(TAG, "Failed to write bond request! " + throwable); + } + }); + + }, throwable -> { + // Could not write keep alive ? retry? + UserError.Log.e(TAG, "Failed writing keep-alive request! " + throwable); + }); + UserError.Log.d(TAG, "Exiting doKeepAliveBondRequest"); + final PowerManager.WakeLock linger = JoH.getWakeLock("jam-g5-bond-linger", 30000); + return true; + } + + @SuppressLint("CheckResult") + public static boolean doReset(Ob1G5CollectionService parent, RxBleConnection connection) { + if (connection == null) return false; + parent.msg("Hard Resetting Transmitter"); + connection.writeCharacteristic(Control, nn(new ResetTxMessage().byteSequence)) + .subscribe(characteristicValue -> { + if (d) + UserError.Log.d(TAG, "Wrote ResetTxMessage request!!"); + parent.msg("Hard Reset Sent"); + }, throwable -> { + parent.msg("Hard Reset maybe Failed"); + UserError.Log.e(TAG, "Failed to write ResetTxMessage: " + throwable); + if (throwable instanceof BleGattCharacteristicException) { + final int status = ((BleGattCharacteristicException) throwable).getStatus(); + UserError.Log.e(TAG, "Got status message: " + getStatusName(status)); + } + }); + return true; + } + + private static void reReadGlucoseData() { + enqueueUniqueCommand(new GlucoseTxMessage(), "Re-read glucose"); + } + + @SuppressLint("CheckResult") + public static void checkVersionAndBattery(final Ob1G5CollectionService parent, final RxBleConnection connection) { + final int nextVersionRequest = requiredNextFirmwareDetailsType(); + if ((getVersionDetails) && (nextVersionRequest != -1)) { + connection.writeCharacteristic(Control, nn(new VersionRequestTxMessage(nextVersionRequest).byteSequence)) + .subscribe(versionValue -> { + UserError.Log.e(TAG, "Wrote version request: " + nextVersionRequest); + }, throwable -> { + UserError.Log.e(TAG, "Failed to write VersionRequestTxMessage: " + throwable); + }); + } else if ((getBatteryDetails) && (parent.getBatteryStatusNow || !haveCurrentBatteryStatus())) { + + enqueueUniqueCommand(new BatteryInfoTxMessage(), "Query battery"); + parent.getBatteryStatusNow = false; + + } + } + + // Get Data + @SuppressLint("CheckResult") + public static boolean doGetData(Ob1G5CollectionService parent, RxBleConnection connection) { + if (connection == null) return false; + // TODO switch modes depending on conditions as to whether we are using internal + final boolean use_g5_internal_alg = Pref.getBooleanDefaultFalse("ob1_g5_use_transmitter_alg"); + UserError.Log.d(TAG, use_g5_internal_alg ? ("Requesting Glucose Data " + (usingG6() ? "G6" : "G5")) : "Requesting Sensor Data"); + + if (!use_g5_internal_alg) { + parent.lastSensorStatus = null; // not applicable + parent.lastUsableGlucosePacketTime = 0; + } + + connection.setupIndication(Control) + + .doOnNext(notificationObservable -> { + + if (d) UserError.Log.d(TAG, "Notifications enabled"); + speakSlowly(); + + connection.writeCharacteristic(Control, nn(use_g5_internal_alg ? (getEGlucose() ? new EGlucoseTxMessage().byteSequence : new GlucoseTxMessage().byteSequence) : new SensorTxMessage().byteSequence)) + .subscribe( + characteristicValue -> { + if (d) + UserError.Log.d(TAG, "Wrote SensorTxMessage request"); + }, throwable -> { + UserError.Log.e(TAG, "Failed to write SensorTxMessage: " + throwable); + if (throwable instanceof BleGattCharacteristicException) { + final int status = ((BleGattCharacteristicException) throwable).getStatus(); + UserError.Log.e(TAG, "Got status message: " + getStatusName(status)); + if (status == 8) { + UserError.Log.e(TAG, "Request rejected due to Insufficient Authorization failure!"); + parent.authResult(false); + } + } + }); + + }) + .flatMap(notificationObservable -> notificationObservable) + .timeout(6, TimeUnit.SECONDS) + .subscribe(bytes -> { + // incoming data notifications + UserError.Log.d(TAG, "Received indication bytes: " + JoH.bytesToHex(bytes)); + final PacketShop data_packet = classifyPacket(bytes); + switch (data_packet.type) { + case SensorRxMessage: + + try { + checkVersionAndBattery(parent, connection); + } finally { + processSensorRxMessage((SensorRxMessage) data_packet.msg); + parent.msg("Got data"); + parent.updateLast(tsl()); + parent.clearErrors(); + } + break; + + case VersionRequest1RxMessage: + if (!setStoredFirmwareBytes(getTransmitterID(), 1, bytes, true)) { + UserError.Log.e(TAG, "Could not save out firmware version!"); + } + nextBackFillCheckSize = BACKFILL_CHECK_LARGE; + if (JoH.ratelimit("g6-evaluate", 600)) { + Inevitable.task("evaluteG6Settings", 10000, () -> evaluateG6Settings()); + } + break; + + case VersionRequestRxMessage: + if (!setStoredFirmwareBytes(getTransmitterID(), 0, bytes, true)) { + UserError.Log.e(TAG, "Could not save out firmware version!"); + } + nextBackFillCheckSize = BACKFILL_CHECK_LARGE; + if (JoH.ratelimit("g6-evaluate", 600)) { + Inevitable.task("evaluteG6Settings", 10000, () -> evaluateG6Settings()); + } + break; + + case VersionRequest2RxMessage: + if (!setStoredFirmwareBytes(getTransmitterID(), 2, bytes, true)) { + UserError.Log.e(TAG, "Could not save out firmware version!"); + } + nextBackFillCheckSize = BACKFILL_CHECK_LARGE; + if (JoH.ratelimit("g6-evaluate", 600)) { + Inevitable.task("evaluteG6Settings", 10000, () -> evaluateG6Settings()); + } + break; + + case BatteryInfoRxMessage: + if (!setStoredBatteryBytes(getTransmitterID(), bytes)) { + UserError.Log.e(TAG, "Could not save out battery data!"); + } else { + if (parent.android_wear) { + PersistentStore.setBoolean(G5_BATTERY_WEARABLE_SEND, true); + } + } + nextBackFillCheckSize = BACKFILL_CHECK_LARGE; + break; + + case SessionStartRxMessage: + final SessionStartRxMessage session_start = (SessionStartRxMessage) data_packet.msg; + if (session_start.isOkay()) { + // TODO persist this + parent.msg("Session Started Successfully: " + JoH.dateTimeText(session_start.getSessionStart()) + " " + JoH.dateTimeText(session_start.getRequestedStart()) + " " + JoH.dateTimeText(session_start.getTransmitterTime())); + DexResetHelper.cancel(); + } else { + final String msg = "Session Start Failed: " + session_start.message(); + parent.msg(msg); + UserError.Log.ueh(TAG, msg); + JoH.showNotification(devName() + " Start Failed", msg, null, Constants.G5_START_REJECT, true, true, false); + UserError.Log.ueh(TAG, "Session Start failed info: " + JoH.dateTimeText(session_start.getSessionStart()) + " " + JoH.dateTimeText(session_start.getRequestedStart()) + " " + JoH.dateTimeText(session_start.getTransmitterTime())); + if (session_start.isFubar()) { + final long tk = DexTimeKeeper.getDexTime(getTransmitterID(), tsl()); + if (tk > 0) { + DexResetHelper.offer("Unusual session start failure, is transmitter crashed? Try Hard Reset?"); + } else { + UserError.Log.e(TAG, "No reset as TimeKeeper reports invalid: " + tk); + } + } + if (Pref.getBooleanDefaultFalse("ob1_g5_restart_sensor") && (Sensor.isActive())) { + if (pratelimit("secondary-g5-start", 1800)) { + UserError.Log.ueh(TAG, "Trying to Start sensor again"); + startSensor(tsl()); + } + } + } + reReadGlucoseData(); + + break; + + case SessionStopRxMessage: + final SessionStopRxMessage session_stop = (SessionStopRxMessage) data_packet.msg; + if (session_stop.isOkay()) { + // TODO persist this + final String msg = "Session Stopped Successfully: " + JoH.dateTimeText(session_stop.getSessionStart()) + " " + JoH.dateTimeText(session_stop.getSessionStop()); + parent.msg(msg); + UserError.Log.ueh(TAG, msg); + reReadGlucoseData(); + enqueueUniqueCommand(new TimeTxMessage(), "Query time after stop"); + } else { + // TODO what does an error when session isn't started look like? Probably best to downgrade those somewhat + final String msg = "Session Stop Failed: packet valid: " + session_stop.isValid() + " Status code: " + session_stop.getStatus(); + UserError.Log.uel(TAG, msg); + } + break; + + case GlucoseRxMessage: + final GlucoseRxMessage glucose = (GlucoseRxMessage) data_packet.msg; + parent.processCalibrationState(glucose.calibrationState()); + + if (glucose.usable()) { + parent.msg("Got " + devName() + " glucose"); + } else { + parent.msg("Got data from " + devName()); + } + + glucoseRxCommon(glucose, parent, connection); + break; + + // TODO base class duplication + case EGlucoseRxMessage: + final EGlucoseRxMessage eglucose = (EGlucoseRxMessage) data_packet.msg; + parent.processCalibrationState(eglucose.calibrationState()); + + if (eglucose.usable()) { + parent.msg("Got G6 glucose"); + } else { + parent.msg("Got data from G6"); + } + + glucoseRxCommon(eglucose, parent, connection); + break; + + + case CalibrateRxMessage: + final CalibrateRxMessage calibrate = (CalibrateRxMessage) data_packet.msg; + if (calibrate.accepted()) { + parent.msg("Calibration accepted"); + UserError.Log.ueh(TAG, "Calibration accepted by transmitter"); + } else { + final String msg = "Calibration rejected: " + calibrate.message(); + UserError.Log.wtf(TAG, msg); + parent.msg(msg); + JoH.showNotification("Calibration rejected", msg, null, Constants.G5_CALIBRATION_REJECT, true, true, false); + } + reReadGlucoseData(); + break; + + case BackFillRxMessage: + final BackFillRxMessage backfill = (BackFillRxMessage) data_packet.msg; + if (backfill.valid()) { + UserError.Log.d(TAG, "Backfill request confirmed"); + } else { + UserError.Log.wtf(TAG, "Backfill request corrupted!"); + } + break; + + case TransmitterTimeRxMessage: + // This message is received every 120-125m + final TransmitterTimeRxMessage txtime = (TransmitterTimeRxMessage) data_packet.msg; + DexTimeKeeper.updateAge(getTransmitterID(), txtime.getCurrentTime(), true); + if (txtime.sessionInProgress()) { + UserError.Log.e(TAG, "Session start time reports: " + + JoH.dateTimeText(txtime.getRealSessionStartTime()) + " Duration: " + + JoH.niceTimeScalar(txtime.getSessionDuration())); + DexSessionKeeper.setStart(txtime.getRealSessionStartTime()); + } else { + UserError.Log.e(TAG, "Session start time reports: No session in progress"); + DexSessionKeeper.clearStart(); + } + if (Pref.getBooleanDefaultFalse("ob1_g5_preemptive_restart")) { + int restartDaysThreshold = usingG6() ? 9 : 6; + if (txtime.getSessionDuration() > Constants.DAY_IN_MS * restartDaysThreshold + && txtime.getSessionDuration() < Constants.MONTH_IN_MS) { + UserError.Log.uel(TAG, "Requesting preemptive session restart"); + restartSensorWithTimeTravel(); + } + } + break; + + case F2DUnknownRxMessage: + UserError.Log.d(TAG,"Received F2D message"); + try { + checkVersionAndBattery(parent, connection); + } finally { + parent.msg("Got no raw"); + parent.updateLast(tsl()); // TODO verify if this is ok to do here + parent.clearErrors(); // TODO verify if this is ok to do here + } + break; + + default: + UserError.Log.e(TAG, "Got unknown packet rx: " + JoH.bytesToHex(bytes)); + break; + } + if (!queued(parent, connection)) { + inevitableDisconnect(parent, connection); + } + + }, throwable -> { + if (!(throwable instanceof OperationSuccess)) { + if (throwable instanceof BleDisconnectedException) { + UserError.Log.d(TAG, "Disconnected when waiting to receive indication: " + throwable); + parent.changeState(Ob1G5CollectionService.STATE.CLOSE); + } else { + UserError.Log.e(TAG, "Error receiving indication: " + throwable); + //throwable.printStackTrace(); + disconnectNow(parent, connection); + } + + } + }); + + + return true; + } + + private static void glucoseRxCommon(final BaseGlucoseRxMessage glucose, final Ob1G5CollectionService parent, final RxBleConnection connection) { + if (JoH.ratelimit("ob1-g5-also-read-raw", 20)) { + //if (FirmwareCapability.isTransmitterRawCapable(getTransmitterID())) { + enqueueUniqueCommand(new SensorTxMessage(), "Also read raw"); + // } + } + + if (JoH.pratelimit("g5-tx-time-since", 7200) + || glucose.calibrationState().warmingUp() + || !DexSessionKeeper.isStarted()) { + if (JoH.ratelimit("g5-tx-time-governer", 30)) { + enqueueUniqueCommand(new TimeTxMessage(), "Periodic Query Time"); + } + } + + // TODO check firmware version + if (glucose.calibrationState().readyForBackfill() && !parent.getBatteryStatusNow) { + backFillIfNeeded(parent, connection); + } + processGlucoseRxMessage(parent, glucose); + parent.updateLast(tsl()); + parent.clearErrors(); + } + + private static void inevitableDisconnect(Ob1G5CollectionService parent, RxBleConnection connection) { + inevitableDisconnect(parent, connection, speakSlowlyDelay()); + } + + private static void inevitableDisconnect(Ob1G5CollectionService parent, RxBleConnection connection, long guardTime) { + Inevitable.task("Ob1G5 disconnect", 500 + guardTime + speakSlowlyDelay(), () -> disconnectNow(parent, connection)); + } + + @SuppressLint("CheckResult") + private static void disconnectNow(Ob1G5CollectionService parent, RxBleConnection connection) { + // tell device to disconnect now + UserError.Log.d(TAG, "Disconnect NOW: " + JoH.dateTimeText(tsl())); + speakSlowly(); + connection.writeCharacteristic(Control, nn(new DisconnectTxMessage().byteSequence)) + .timeout(2, TimeUnit.SECONDS) + // .observeOn(Schedulers.newThread()) + // .subscribeOn(Schedulers.newThread()) + .subscribe(disconnectValue -> { + if (d) UserError.Log.d(TAG, "Wrote disconnect request"); + parent.changeState(Ob1G5CollectionService.STATE.CLOSE); + throw new OperationSuccess("Requested Disconnect"); + }, throwable -> { + if (!(throwable instanceof OperationSuccess)) { + UserError.Log.d(TAG, "Disconnect NOW failure: " + JoH.dateTimeText(tsl())); + if (throwable instanceof BleDisconnectedException) { + UserError.Log.d(TAG, "Failed to write DisconnectTxMessage as already disconnected: " + throwable); + + } else { + UserError.Log.e(TAG, "Failed to write DisconnectTxMessage: " + throwable); + + } + parent.changeState(Ob1G5CollectionService.STATE.CLOSE); + } + }); + UserError.Log.d(TAG, "Disconnect NOW exit: " + JoH.dateTimeText(tsl())); + } + + private static void backFillIfNeeded(Ob1G5CollectionService parent, RxBleConnection connection) { + final int check_readings = nextBackFillCheckSize; + UserError.Log.d(TAG, "Checking " + check_readings + " for backfill requirement"); + final List lastReadings = BgReading.latest_by_size(check_readings); + boolean ask_for_backfill = false; + long earliest_timestamp = tsl() - MAX_BACKFILL_PERIOD_MS; + long latest_timestamp = tsl(); + if ((lastReadings == null) || (lastReadings.size() != check_readings)) { + ask_for_backfill = true; + } else { + for (int i = 0; i < lastReadings.size(); i++) { + final BgReading reading = lastReadings.get(i); + if ((reading == null) || (msSince(reading.timestamp) > ((DEXCOM_PERIOD * i) + Constants.MINUTE_IN_MS * 7))) { + ask_for_backfill = true; + if ((reading != null) && (msSince(reading.timestamp) <= MAX_BACKFILL_PERIOD_MS)) { + earliest_timestamp = reading.timestamp; + } + if (reading != null) { + UserError.Log.d(TAG, "Flagging backfill tripped by reading: " + i + " at time: " + JoH.dateTimeText(reading.timestamp) + " creating backfill window: " + JoH.dateTimeText(earliest_timestamp)); + } else { + UserError.Log.d(TAG, "Flagging backfill tripped by null reading: " + i); + } + break; + } else { + // good record + latest_timestamp = reading.timestamp; + } + } + } + + if (ask_for_backfill) { + nextBackFillCheckSize = BACKFILL_CHECK_LARGE; + monitorBackFill(parent, connection); + final long startTime = earliest_timestamp - (Constants.MINUTE_IN_MS * 5); + final long endTime = latest_timestamp + (Constants.MINUTE_IN_MS * 5); + UserError.Log.d(TAG, "Requesting backfill between: " + JoH.dateTimeText(startTime) + " " + JoH.dateTimeText(endTime)); + enqueueUniqueCommand( + BackFillTxMessage.get(getTransmitterID(), startTime, endTime), + "Get backfill since: " + JoH.hourMinuteString(startTime)); + } else { + nextBackFillCheckSize = BACKFILL_CHECK_SMALL; + } + } + + private static void enqueueCommand(BaseMessage tm, String msg) { + if (tm != null) { + final Ob1Work item = new Ob1Work(tm, msg); + synchronized (commandQueue) { + commandQueue.add(item); + } + streamCheck(item); + backupCheck(item); + } + } + + private static void streamCheck(Ob1Work item) { + if (item.streamable()) { + Inevitable.task("check wear stream", 5000, WatchUpdaterService::checkOb1Queue); + } + } + + private static void backupCheck(Ob1Work item) { + if (item.streamable()) { + saveQueue(); + } + } + + private static void enqueueUniqueCommand(BaseMessage tm, String msg) { + if (tm != null) { + final Class searchClass = tm.getClass(); + Ob1Work item; + synchronized (commandQueue) { + if (searchQueue(searchClass)) { + UserError.Log.d(TAG, "Not adding duplicate: " + searchClass.getSimpleName()); + return; + } + item = new Ob1Work(tm, msg); + if (d) { + UserError.Log.d(TAG, "Adding to queue packet: " + msg + " " + HexDump.dumpHexString(tm.byteSequence)); + } + commandQueue.add(item); + streamCheck(item); + } + backupCheck(item); + } + } + + private static boolean queueContains(BaseMessage tm) { + final Class searchClass = tm.getClass(); + return queueContains(searchClass); + } + + private static boolean queueContains(Class searchClass) { + synchronized (commandQueue) { + return searchQueue(searchClass); + } + } + + // note not synchronized here + private static boolean searchQueue(Class searchClass) { + for (Ob1Work item : commandQueue) { + if (item.msg.getClass() == searchClass) { + return true; + } + } + return false; + } + + public static void restoreQueue() { + if (!backup_loaded) { + loadQueue(); + } + } + + private synchronized static void loadQueue() { + if (commandQueue.size() == 0) { + injectQueueJson(PersistentStore.getString(PREF_SAVED_QUEUE)); + UserError.Log.d(TAG, "Loaded queue stream backup."); + } + backup_loaded = true; + } + + + private static void saveQueue() { + final String queue_json = extractQueueJson(); + if (!(queue_json == null ? "" : queue_json).equals(PersistentStore.getString(PREF_SAVED_QUEUE))) { + PersistentStore.setString(PREF_SAVED_QUEUE, queue_json); + UserError.Log.d(TAG, "Saved queue stream backup: " + queue_json); + } + } + + public static String extractQueueJson() { + synchronized (commandQueue) { + final List queue = new ArrayList<>(commandQueue.size()); + for (Ob1Work item : commandQueue) { + if (item.streamable()) queue.add(item); + } + return JoH.defaultGsonInstance().toJson(queue); + } + } + + // used in backup restore and wear + @SuppressWarnings("WeakerAccess") + public static void injectQueueJson(String json) { + if (json == null || json.length() == 0) return; + final Type queueType = new TypeToken>() { + }.getType(); + final List queue = JoH.defaultGsonInstance().fromJson(json, queueType); + synchronized (commandQueue) { + commandQueue.clear(); + commandQueue.addAll(queue); + } + UserError.Log.d(TAG, "Replaced queue with stream: " + json); + } + + public static String extractDexTime() { + return DexTimeKeeper.extractForStream(getTransmitterID()); + } + + @SuppressWarnings("unused") + public static void injectDexTime(String stream) { + DexTimeKeeper.injectFromStream(stream); + } + + + public static boolean pendingStop() { + return queueContains(SessionStopTxMessage.class); + } + + public static boolean pendingStart() { + return queueContains(SessionStartTxMessage.class); + } + + public static boolean pendingCalibration() { + return queueContains(CalibrateTxMessage.class); + } + + public static int queueSize() { + return commandQueue.size(); + } + + public static void emptyQueue() { + synchronized (commandQueue) { + if (commandQueue.size() > 0) { + UserError.Log.d(TAG, "Queue drained on wear, clearing: " + commandQueue.size() + " commands"); + commandQueue.clear(); + Inevitable.task("Save cleared G5 queue", 1000, Ob1G5StateMachine::saveQueue); + } else { + if (d) UserError.Log.d(TAG, "Local command queue is already empty"); + } + } + } + + public static boolean deleteFirstQueueCalibration(final int mgdl) { + synchronized (commandQueue) { + final Ob1Work item = commandQueue.peek(); + if (item != null) { + if (item.msg instanceof CalibrateTxMessage) { + final CalibrateTxMessage cal = (CalibrateTxMessage) item.msg; + if (mgdl == -1 || cal.glucose == mgdl) { + commandQueue.poll(); // eat this entry + return true; + } + } + } + } + return false; + } + + public static String getFirstQueueItemName() { + synchronized (commandQueue) { + final Ob1Work item = commandQueue.peek(); + return item != null ? item.text : ""; + } + } + + private static boolean acceptCommands() { + return DexCollectionType.hasDexcomRaw() && Pref.getBooleanDefaultFalse("ob1_g5_use_transmitter_alg"); + } + + // actual limit is something like 20-30 mins but due to propagation delays its too risky to adjust + private static final long MAX_START_TIME_REWIND = Constants.MINUTE_IN_MS * 5; + + public static void startSensor(long when) { + if (acceptCommands()) { + if (msSince(when) > MAX_START_TIME_REWIND) { + when = tsl() - MAX_START_TIME_REWIND; + UserError.Log.e(TAG, "Cannot rewind sensor start time beyond: " + JoH.dateTimeText(when)); + } + if (usingG6()) { + final String code = G6CalibrationParameters.getCurrentSensorCode(); + if (code == null) { + UserError.Log.wtf(TAG, "Cannot start G6 sensor as calibration code not set!"); + } else { + UserError.Log.ueh(TAG, "Starting G6 sensor using calibration code: " + code); + enqueueUniqueCommand(new SessionStartTxMessage(when, + DexTimeKeeper.getDexTime(getTransmitterID(), when), code), + "Start G6 Sensor"); + } + + } else { + UserError.Log.ueh(TAG, "Starting G5 sensor"); + enqueueUniqueCommand(new SessionStartTxMessage(when, + DexTimeKeeper.getDexTime(getTransmitterID(), when)), + "Start G5 Sensor"); + } + } + } + + private static void reprocessTxMessage(BaseMessage tm) { + // rewrite session start messages in case our clock was wrong + if (tm instanceof SessionStartTxMessage) { + final SessionStartTxMessage ssm = (SessionStartTxMessage) tm; + if (usingG6()) { + final String code = G6CalibrationParameters.getCurrentSensorCode(); + if (code == null) { + UserError.Log.wtf(TAG, "Cannot reprocess start G6 sensor as calibration code not set!"); + } else { + // g6 + tm.byteSequence = new SessionStartTxMessage(ssm.getStartTime(), DexTimeKeeper.getDexTime(getTransmitterID(), ssm.getStartTime()), code).byteSequence; + } + } else { + // g5 + tm.byteSequence = new SessionStartTxMessage(ssm.getStartTime(), DexTimeKeeper.getDexTime(getTransmitterID(), ssm.getStartTime())).byteSequence; + } + UserError.Log.d(TAG, "New session start: " + ssm.getDexTime() + " for time: " + JoH.dateTimeText(ssm.getStartTime())); + if (d) { + UserError.Log.d(TAG, "New packet: " + HexDump.dumpHexString(tm.byteSequence)); + } + } + } + + + public static void stopSensor() { + if (acceptCommands()) { + enqueueCommand( + new SessionStopTxMessage( + DexTimeKeeper.getDexTime(getTransmitterID(), tsl())), + "Stop Sensor"); + } + } + + + public static void restartSensorWithTimeTravel() { + restartSensorWithTimeTravel(tsl() - + (useExtendedTimeTravel() ? DAY_IN_MS * 3 + HOUR_IN_MS * 2 : HOUR_IN_MS * 2 - MINUTE_IN_MS * 10)); + } + + public static boolean useExtendedTimeTravel() { + return Pref.getBooleanDefaultFalse("ob1_g5_preemptive_restart_extended_time_travel") + && (FirmwareCapability.isTransmitterTimeTravelCapable(getTransmitterID()) + || (Pref.getBooleanDefaultFalse("ob1_g5_defer_preemptive_restart_all_firmwares") && Home.get_engineering_mode())); + } + + public static void restartSensorWithTimeTravel(long when) { + if (acceptCommands()) { + enqueueUniqueCommand( + new SessionStopTxMessage( + DexTimeKeeper.getDexTime(getTransmitterID(), when)), + "Auto Stop Sensor"); + final long when_started = when + SECOND_IN_MS; + enqueueUniqueCommand(new SessionStartTxMessage(when, + DexTimeKeeper.getDexTime(getTransmitterID(), when_started)), + "Auto Start Sensor"); + if (Pref.getBoolean("ob1_g5_preemptive_restart_alert", true)) { + Notifications.ob1SessionRestartRequested(); + } + Treatments.create_note(xdrip.getAppContext().getString(R.string.ob1_session_restarted_note), JoH.tsl()); + } + } + + public static void addCalibration(final int glucose, long timestamp) { + if (acceptCommands()) { + long since = msSince(timestamp); + if (since < 0) { + final String msg = "Cannot send calibration in future to transmitter: " + glucose + " @ " + JoH.dateTimeText(timestamp); + JoH.static_toast_long(msg); + UserError.Log.wtf(TAG, msg); + return; + } + if (since > HOUR_IN_MS) { + final String msg = "Cannot send calibration older than 1 hour to transmitter: " + glucose + " @ " + JoH.dateTimeText(timestamp); + JoH.static_toast_long(msg); + UserError.Log.wtf(TAG, msg); + return; + } + if ((glucose < 40 || glucose > 400)) { + final String msg = "Calibration glucose value out of range: " + glucose; + JoH.static_toast_long(msg); + UserError.Log.wtf(TAG, msg); + return; + } + + UserError.Log.uel(TAG, "Queuing Calibration for transmitter: " + BgGraphBuilder.unitized_string_with_units_static(glucose) + " " + JoH.dateTimeText(timestamp)); + + enqueueCommand(new CalibrateTxMessage( + glucose, DexTimeKeeper.getDexTime(getTransmitterID(), timestamp)), + "Calibrate " + BgGraphBuilder.unitized_string_with_units_static_short(glucose)); + } + } + + private static boolean queued(Ob1G5CollectionService parent, RxBleConnection connection) { + if (!commandQueue.isEmpty()) { + processQueueCommand(parent, connection); + return true; + } + return false; + } + + @SuppressLint("CheckResult") + private static void processQueueCommand(Ob1G5CollectionService parent, RxBleConnection connection) { + boolean changed = false; + synchronized (commandQueue) { + if (!commandQueue.isEmpty()) { + final Ob1Work unit = commandQueue.poll(); + if (unit != null) { + changed = true; + reprocessTxMessage(unit.msg); + if (unit.retry < 5 && JoH.msSince(unit.timestamp) < HOUR_IN_MS * 8) { + connection.writeCharacteristic(Control, nn(unit.msg.byteSequence)) + .timeout(2, TimeUnit.SECONDS) + .subscribe(value -> { + UserError.Log.d(TAG, "Wrote Queue Message: " + unit.text); + final long guardTime = unit.msg.guardTime(); + inevitableDisconnect(parent, connection, guardTime); + if (guardTime > 0) { + UserError.Log.d(TAG, "Sleeping post execute: " + unit.text + " " + guardTime + "ms"); + JoH.threadSleep(guardTime); + } + throw new OperationSuccess("Completed: " + unit.text); + + }, throwable -> { + if (!(throwable instanceof OperationSuccess)) { + unit.retry++; + UserError.Log.d(TAG, "Re-adding: " + unit.text); + synchronized (commandQueue) { + commandQueue.push(unit); + } + UserError.Log.d(TAG, "Failure: " + unit.text + " " + JoH.dateTimeText(tsl())); + if (throwable instanceof BleDisconnectedException) { + UserError.Log.d(TAG, "Disconnected: " + unit.text + " " + throwable); + parent.changeState(Ob1G5CollectionService.STATE.CLOSE); + } else { + UserError.Log.e(TAG, "Failed to write: " + unit.text + " " + throwable); + } + parent.changeState(Ob1G5CollectionService.STATE.CLOSE); + } else { + queued(parent, connection); // turtles all the way down + } + }); + } else { + UserError.Log.e(TAG, "Ejected command from queue due to being too old: " + unit.text + " " + JoH.dateTimeText(unit.timestamp)); + queued(parent, connection); // move on to next command if we just ejected something + } + } + if (commandQueue.isEmpty()) { + if (d) UserError.Log.d(TAG, "Command Queue Drained"); + if (android_wear) { + PersistentStore.setBoolean(PREF_QUEUE_DRAINED, true); + } + } + } else { + UserError.Log.d(TAG, "Command Queue is Empty"); + } + } + if (changed) saveQueue(); + } + + private static void processGlucoseRxMessage(Ob1G5CollectionService parent, final BaseGlucoseRxMessage glucose) { + if (glucose == null) return; + lastGlucosePacket = tsl(); + DexTimeKeeper.updateAge(getTransmitterID(), glucose.timestamp); + if (glucose.usable() || (glucose.insufficient() && Pref.getBoolean("ob1_g5_use_insufficiently_calibrated", true))) { + UserError.Log.d(TAG, "Got usable glucose data from G5!!"); + final BgReading bgReading = BgReading.bgReadingInsertFromG5(glucose.glucose, tsl()); + if (bgReading != null) { + try { + bgReading.calculated_value_slope = glucose.getTrend() / Constants.MINUTE_IN_MS; // note this is different to the typical calculated slope, (normally delta) + if (bgReading.calculated_value_slope == Double.NaN) { + bgReading.hide_slope = true; + } + } catch (Exception e) { + // not a good number - does this exception ever actually fire? + } + if (!FirmwareCapability.isTransmitterRawCapable(getTransmitterID())) { + bgReading.noRawWillBeAvailable(); + } + if (glucose.insufficient()) { + bgReading.appendSourceInfo("Insufficient").save(); + } + } else { + UserError.Log.wtf(TAG, "New BgReading was null in processGlucoseRxMessage!"); + } + lastGlucoseBgReading = bgReading; + lastUsableGlucosePacket = lastGlucosePacket; + parent.lastUsableGlucosePacketTime = lastUsableGlucosePacket; + if (glucose.getPredictedGlucose() != null) { + // not really supported on wear yet + if (!android_wear) { + Prediction.create(tsl(), glucose.getPredictedGlucose(), "EGlucoseRx").save(); + } + } + + if (android_wear && wear_broadcast && bgReading != null) { + // emit local broadcast + BroadcastGlucose.sendLocalBroadcast(bgReading); + } + + if (WholeHouse.isLive()) { + Mimeograph.poll(false); + } + + } else { + // TODO this is duplicated in processCalibrationState() + if (glucose.calibrationState().sensorFailed()) { + if (JoH.pratelimit("G5 Sensor Failed", 3600 * 3)) { + JoH.showNotification(devName() + " SENSOR FAILED", "Sensor reporting failed", null, Constants.G5_SENSOR_ERROR, true, true, false); + } + } + } + } + + + private static void processSensorRxMessage(SensorRxMessage sensorRx) { + if (sensorRx == null) return; + + // TODO, is this accurate or needed? + int sensor_battery_level = 0; + if (sensorRx.status == TransmitterStatus.BRICKED) { + sensor_battery_level = 206; //will give message "EMPTY" + } else if (sensorRx.status == TransmitterStatus.LOW) { + sensor_battery_level = 209; //will give message "LOW" + } else { + sensor_battery_level = 216; //no message, just system status "OK" + } + + UserError.Log.d(TAG, "SUCCESS!! unfiltered: " + sensorRx.unfiltered + " filtered: " + sensorRx.filtered + " timestamp: " + sensorRx.timestamp + " " + JoH.qs((double) sensorRx.timestamp / 86400, 1) + " days :: (" + sensorRx.status + ")"); + DexTimeKeeper.updateAge(getTransmitterID(), sensorRx.timestamp); + Ob1G5CollectionService.setLast_transmitter_timestamp(sensorRx.timestamp); + if (sensorRx.unfiltered == 0) { + UserError.Log.e(TAG, "Transmitter sent raw sensor value of 0 !! This isn't good. " + JoH.hourMinuteString()); + } else { + // final boolean g6 = usingG6(); + // final boolean g6r2 = g6 && FirmwareCapability.isTransmitterG6Rev2(getTransmitterID()); + // processNewTransmitterData(g6 ? (int)(sensorRx.unfiltered * (g6r2 ? G6_REV2_SCALING : G6_SCALING)) : sensorRx.unfiltered, g6 ? (int)(sensorRx.filtered * (g6r2 ? G6_REV2_SCALING : G6_SCALING)) : sensorRx.filtered, sensor_battery_level, new Date().getTime()); + processNewTransmitterData((int) RawScaling.scale(sensorRx.unfiltered, getTransmitterID(), false), + (int) RawScaling.scale(sensorRx.filtered, getTransmitterID(), true), + sensor_battery_level, new Date().getTime()); + } + + if (WholeHouse.isLive()) { + Mimeograph.poll(false); + } + } + + // Save/process the data in xDrip style + private static synchronized void processNewTransmitterData(int raw_data, int filtered_data, int sensor_battery_level, long captureTime) { + + final TransmitterData transmitterData = TransmitterData.create(raw_data, filtered_data, sensor_battery_level, captureTime); + if (transmitterData == null) { + UserError.Log.e(TAG, "TransmitterData.create failed: Duplicate packet"); + return; + } else { + UserError.Log.d(TAG, "Created transmitter data " + transmitterData.uuid + " " + JoH.dateTimeText(transmitterData.timestamp)); + // TODO timeInMillisecondsOfLastSuccessfulSensorRead = captureTime; + } + + if (transmitterData.unchangedRaw() && !SensorSanity.allowTestingWithDeadSensor()) { + UserError.Log.wtf(TAG, "Raw values are not changing - blocking further processing: " + raw_data + " " + filtered_data); + return; + } + + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + UserError.Log.e(TAG, "setSerialDataToTransmitterRawData: No Active Sensor, Data only stored in Transmitter Data"); + return; + } + + //TODO : LOG if unfiltered or filtered values are zero + + Sensor.updateBatteryLevel(sensor, transmitterData.sensor_battery_level); + if (d) + UserError.Log.i(TAG, "timestamp create: " + Long.toString(transmitterData.timestamp)); + + if ((lastGlucoseBgReading != null) && (msSince(lastUsableGlucosePacket) < Constants.SECOND_IN_MS * 30)) { + UserError.Log.d(TAG, "Updating BgReading provided by transmitter"); + // use sensor data to update previous record instead of trying to calculate with it + lastGlucoseBgReading.raw_data = transmitterData.raw_data / 1000; + lastGlucoseBgReading.filtered_data = transmitterData.filtered_data / 1000; + // TODO calculate filtered calculated value from internal alg?? + lastGlucoseBgReading.calculateAgeAdjustedRawValue(); + lastGlucoseBgReading.save(); + } else { + if (!Ob1G5CollectionService.usingNativeMode() || Ob1G5CollectionService.fallbackToXdripAlgorithm() || BgReading.latest(3).size() < 3) { + final BgReading bgreading = BgReading.create(transmitterData.raw_data, transmitterData.filtered_data, xdrip.getAppContext(), transmitterData.timestamp); + UserError.Log.d(TAG, "BgReading created: " + bgreading.uuid + " " + JoH.dateTimeText(bgreading.timestamp)); + } + } + + // UserError.Log.d(TAG, "Dex raw_data " + Double.toString(transmitterData.raw_data));//KS + // UserError.Log.d(TAG, "Dex filtered_data " + Double.toString(transmitterData.filtered_data));//KS + // UserError.Log.d(TAG, "Dex sensor_battery_level " + Double.toString(transmitterData.sensor_battery_level));//KS + // UserError.Log.d(TAG, "Dex timestamp " + JoH.dateTimeText(transmitterData.timestamp));//KS + + + // TODO static_last_timestamp = transmitterData.timestamp; + + } + + public static void evaluateG6Settings() { + if (haveFirmwareDetails()) { + if (FirmwareCapability.isTransmitterG6(getTransmitterID())) { + if (!usingG6()) { + Ob1G5CollectionService.setG6Defaults(); + JoH.showNotification("Enabled G6", "G6 Features and default settings automatically enabled", null, Constants.G6_DEFAULTS_MESSAGE, false, true, false); + } else if (!onlyUsingNativeMode() && !Home.get_engineering_mode()) { + // TODO revisit this now that there is scaling + Ob1G5CollectionService.setG6Defaults(); + JoH.showNotification("Enabled G6", "G6 Native mode enabled", null, Constants.G6_DEFAULTS_MESSAGE, false, true, false); + } + } + } + } + + private static boolean haveFirmwareDetails() { + return getTransmitterID().length() == 6 && getStoredFirmwareBytes(getTransmitterID()).length >= 10; + } + + + private static int requiredNextFirmwareDetailsType() { + final String txid = getTransmitterID(); + if (txid.length() == 6) { + final byte[] v1b = getStoredFirmwareBytes(txid,1); + if (v1b.length < 10) return 1; + final byte[] v0b = getStoredFirmwareBytes(txid,0); + if (v0b.length < 10) return 0; + final byte[] v2b = getStoredFirmwareBytes(txid,2); + if (v2b.length < 10) return 2; + } + return -1; // nothing required + } + + + private static boolean haveCurrentBatteryStatus() { + return getTransmitterID().length() == 6 && (msSince(PersistentStore.getLong(G5_BATTERY_FROM_MARKER + getTransmitterID())) < BATTERY_READ_PERIOD_MS); + } + + private static byte[] getStoredFirmwareBytes(final String transmitterId) { + return getStoredFirmwareBytes(transmitterId, 1); + } + + private static byte[] getStoredFirmwareBytes(final String transmitterId, final int type) { + if (transmitterId.length() != 6) return new byte[0]; + return PersistentStore.getBytes(G5_FIRMWARE_MARKER + transmitterId + "-" + type); + } + + // from wear sync + public static boolean setStoredFirmwareBytes(String transmitterId, byte[] data) { + return setStoredFirmwareBytes(transmitterId, data, false); + } + + public static boolean setStoredFirmwareBytes(String transmitterId, byte[] data, boolean from_bluetooth) { + return setStoredFirmwareBytes(transmitterId, 1, data, from_bluetooth); + } + + public static boolean setStoredFirmwareBytes(String transmitterId, int type, byte[] data, boolean from_bluetooth) { + if (from_bluetooth) UserError.Log.e(TAG, "Store: VersionRX dbg: " + JoH.bytesToHex(data)); + if (transmitterId.length() != 6) return false; + if (data.length < 10) return false; + if (JoH.ratelimit("store-firmware-bytes" + type, 60)) { + PersistentStore.setBytes(G5_FIRMWARE_MARKER + transmitterId + "-" + type, data); + } + return true; + } + + + public synchronized static boolean setStoredBatteryBytes(String transmitterId, byte[] data) { + UserError.Log.e(TAG, "Store: BatteryRX dbg: " + JoH.bytesToHex(data)); + if (transmitterId.length() != 6) return false; + if (data.length < 10) return false; + final BatteryInfoRxMessage batteryInfoRxMessage = new BatteryInfoRxMessage(data); + UserError.Log.e(TAG, "Saving battery data: " + batteryInfoRxMessage.toString()); + PersistentStore.setBytes(G5_BATTERY_MARKER + transmitterId, data); + PersistentStore.setLong(G5_BATTERY_FROM_MARKER + transmitterId, tsl()); + + // TODO logic also needs to handle battery replacements of same transmitter id + final long old_level = PersistentStore.getLong(G5_BATTERY_LEVEL_MARKER + transmitterId); + if ((batteryInfoRxMessage.voltagea < old_level) || (old_level == 0)) { + if (batteryInfoRxMessage.voltagea < LOW_BATTERY_WARNING_LEVEL) { + if (JoH.pratelimit("g5-low-battery-warning", 40000)) { + final boolean loud = !PowerStateReceiver.is_power_connected(); + JoH.showNotification("G5 Battery Low", "G5 Transmitter battery has dropped to: " + batteryInfoRxMessage.voltagea + " it may fail soon", + null, 770, NotificationChannels.LOW_TRANSMITTER_BATTERY_CHANNEL, loud, loud, null, null, null); + } + } + PersistentStore.setLong(G5_BATTERY_LEVEL_MARKER + transmitterId, batteryInfoRxMessage.voltagea); + } + return true; + } + + public static BatteryInfoRxMessage getBatteryDetails(final String tx_id) { + try { + final byte[] batteryStoredBytes = PersistentStore.getBytes(G5_BATTERY_MARKER + tx_id); + return batteryStoredBytes.length > 0 ? new BatteryInfoRxMessage(batteryStoredBytes) : null; + } catch (Exception e) { + if (JoH.quietratelimit("bi-exception", 15)) + UserError.Log.e(TAG, "Exception in getBatteryDetails: " + e); + return null; + } + } + + public static VersionRequest1RxMessage getFirmwareDetails(String tx_id) { + if (tx_id == null) { + if (JoH.quietratelimit("txid-null", 15)) + UserError.Log.e(TAG, "TX ID is null in getFirmwareDetails"); + return null; + } + try { + byte[] stored = getStoredFirmwareBytes(tx_id); + if ((stored != null) && (stored.length > 9)) { + return new VersionRequest1RxMessage(stored); + } + } catch (Exception e) { + if (JoH.quietratelimit("fi-exception", 15)) + UserError.Log.e(TAG, "Exception in getFirmwareDetails: " + e); + return null; + } + return null; + } + + public static BaseMessage getFirmwareXDetails(final String tx_id, final int type) { + if (tx_id == null) { + if (JoH.quietratelimit("txid-null", 15)) + UserError.Log.e(TAG, "TX ID is null in getFirmwareXDetails"); + return null; + } + try { + byte[] stored = getStoredFirmwareBytes(tx_id,type); + if ((stored != null) && (stored.length > 9)) { + switch (type) { + case 1: + return new VersionRequest1RxMessage(stored); + case 2: + return new VersionRequest2RxMessage(stored); + default: + return new VersionRequestRxMessage(stored); + } + } + } catch (Exception e) { + if (JoH.quietratelimit("fi-exception", 15)) + UserError.Log.e(TAG, "Exception in getFirmwareDetails: " + e); + return null; + } + return null; + } + + public static String getRawFirmwareVersionString(final String tx_id) { + final VersionRequest1RxMessage vr = getFirmwareDetails(tx_id); + if (vr != null) { + if (vr.firmware_version_string == null) { + UserError.Log.d(TAG,"Clearing firmware version as evaluated to null"); + setStoredFirmwareBytes(tx_id, new byte[0], false); + return "error"; + } + return vr.firmware_version_string; + } else { + return ""; + } + } + + private static void updateStreamedTillTimeForBackfill() { + // interact with ListenerService + if (JoH.areWeRunningOnAndroidWear()) { + final String pref_last_send_previous = "last_send_previous"; + final long last_send_previous = PersistentStore.getLong(pref_last_send_previous); + PersistentStore.setLong(pref_last_send_previous, Math.min(last_send_previous, tsl() - MAX_BACKFILL_PERIOD_MS)); + } + } + + private static void processBacksies(List backsies) { + boolean changed = false; + for (BackFillStream.Backsie backsie : backsies) { + final long time = DexTimeKeeper.fromDexTime(getTransmitterID(), backsie.getDextime()); + + final long since = JoH.msSince(time); + if ((since > HOUR_IN_MS * 6) || (since < 0)) { + UserError.Log.wtf(TAG, "Backfill timestamp unrealistic: " + JoH.dateTimeText(time) + " (ignored)"); + } else { + if (BgReading.getForPreciseTimestamp(time, Constants.MINUTE_IN_MS * 4) == null) { + final BgReading bgr = BgReading.bgReadingInsertFromG5(backsie.getGlucose(), time, "Backfill"); + lastGlucoseBgReading = bgr; + UserError.Log.d(TAG, "Adding backfilled reading: " + JoH.dateTimeText(time) + " " + BgGraphBuilder.unitized_string_static(backsie.getGlucose())); + changed = true; + } + UserError.Log.d(TAG, "Backsie: " + JoH.dateTimeText(time) + " " + BgGraphBuilder.unitized_string_static(backsie.getGlucose())); + } + } + if (changed) { + updateStreamedTillTimeForBackfill(); + } + } + + private static void monitorBackFill(Ob1G5CollectionService parent, RxBleConnection connection) { + if (d) UserError.Log.d(TAG, "monitor backfill enter"); + + final BackFillStream backfill = new BackFillStream(); + + connection.setupNotification(ProbablyBackfill) + .timeout(15, TimeUnit.SECONDS) // WARN + .observeOn(Schedulers.newThread()) + .flatMap(notificationObservable -> notificationObservable) + .subscribe(bytes -> { + UserError.Log.d(TAG, "Received backfill notification bytes: " + JoH.bytesToHex(bytes)); + backfill.push(bytes); + inevitableDisconnect(parent, connection); + Inevitable.task("Process G5 backfill", 3000, () -> processBacksies(backfill.decode())); + }, throwable -> { + UserError.Log.d(TAG, "backfill throwable: " + throwable); + } + ); + if (d) UserError.Log.d(TAG, "monitor backfill exit"); + } + + private static synchronized byte[] calculateChallengeHash(final byte[] challenge) { + if (challenge == null || challenge.length != 8) { + UserError.Log.e(TAG, "Challenge length must be 8"); + return null; + } + + final byte[] key = getCryptKey(); + if (key == null) { + return null; + } + + final byte[] plainText = new byte[16]; + System.arraycopy(challenge, 0, plainText, 0, 8); + System.arraycopy(challenge, 0, plainText, 8, 8); + + try { + final SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + @SuppressLint("GetInstance") final Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS7Padding"); + aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + return Arrays.copyOfRange(aesCipher.doFinal(plainText, 0, plainText.length), 0, 8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) { + UserError.Log.wtf(TAG, "System Encryption problem: " + e); + return null; + } + + } + + private static byte[] getCryptKey() { + final String transmitterId = getTransmitterID(); + if (transmitterId.length() != 6) + UserError.Log.e(TAG, "cryptKey: Wrong transmitter id length!: " + transmitterId.length()); + try { + final String padding = "00"; + return (padding + transmitterId + padding + transmitterId).getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + UserError.Log.wtf(TAG, "System encoding problem: " + e); + } + return null; + } + + // types of packet we receive + private enum PACKET { + NULL, + UNKNOWN, + AuthChallengeRxMessage, + AuthStatusRxMessage, + SensorRxMessage, + VersionRequestRxMessage, + VersionRequest1RxMessage, + VersionRequest2RxMessage, + BatteryInfoRxMessage, + SessionStartRxMessage, + SessionStopRxMessage, + GlucoseRxMessage, + EGlucoseRxMessage, + CalibrateRxMessage, + BackFillRxMessage, + TransmitterTimeRxMessage, + BondRequestRxMessage, + F2DUnknownRxMessage, + InvalidRxMessage, + + } + + // unified data structure + private static class PacketShop { + private PACKET type; + private BaseMessage msg; + + PacketShop(PACKET type, BaseMessage msg) { + this.type = type; + this.msg = msg; + } + } + + // work out what type of packet we received and wrap it up nicely + private static PacketShop classifyPacket(byte[] packet) { + if ((packet == null) || (packet.length == 0)) return new PacketShop(PACKET.NULL, null); + switch ((int) packet[0]) { + case AuthChallengeRxMessage.opcode: + return new PacketShop(PACKET.AuthChallengeRxMessage, new AuthChallengeRxMessage(packet)); + case AuthStatusRxMessage.opcode: + return new PacketShop(PACKET.AuthStatusRxMessage, new AuthStatusRxMessage(packet)); + case SensorRxMessage.opcode: + return new PacketShop(PACKET.SensorRxMessage, new SensorRxMessage(packet)); + case VersionRequestRxMessage.opcode: + return new PacketShop(PACKET.VersionRequestRxMessage, new VersionRequestRxMessage(packet)); + case VersionRequest1RxMessage.opcode: + return new PacketShop(PACKET.VersionRequest1RxMessage, new VersionRequest1RxMessage(packet)); + case VersionRequest2RxMessage.opcode: + return new PacketShop(PACKET.VersionRequest2RxMessage, new VersionRequest2RxMessage(packet)); + case BatteryInfoRxMessage.opcode: + return new PacketShop(PACKET.BatteryInfoRxMessage, new BatteryInfoRxMessage(packet)); + case SessionStartRxMessage.opcode: + return new PacketShop(PACKET.SessionStartRxMessage, new SessionStartRxMessage(packet, getTransmitterID())); + case SessionStopRxMessage.opcode: + return new PacketShop(PACKET.SessionStopRxMessage, new SessionStopRxMessage(packet, getTransmitterID())); + case GlucoseRxMessage.opcode: + return new PacketShop(PACKET.GlucoseRxMessage, new GlucoseRxMessage(packet)); + case EGlucoseRxMessage.opcode: + return new PacketShop(PACKET.EGlucoseRxMessage, new EGlucoseRxMessage(packet)); + case CalibrateRxMessage.opcode: + return new PacketShop(PACKET.CalibrateRxMessage, new CalibrateRxMessage(packet)); + case BackFillRxMessage.opcode: + return new PacketShop(PACKET.BackFillRxMessage, new BackFillRxMessage(packet)); + case TransmitterTimeRxMessage.opcode: + return new PacketShop(PACKET.TransmitterTimeRxMessage, new TransmitterTimeRxMessage(packet)); + case BondRequestTxMessage.opcode: + return new PacketShop(PACKET.BondRequestRxMessage, null); + case F2DUnknownRxMessage.opcode: + return new PacketShop(PACKET.F2DUnknownRxMessage, new F2DUnknownRxMessage(packet)); + case InvalidRxMessage.opcode: + return new PacketShop(PACKET.InvalidRxMessage, new InvalidRxMessage(packet)); + + } + return new PacketShop(PACKET.UNKNOWN, null); + } + + private static int getTokenSize() { + return 8; + } + + public static boolean usingG6() { + return Pref.getBooleanDefaultFalse("using_g6"); + } + + private static boolean getEGlucose() { + // if (android_wear) { + return usingG6() && Pref.getBooleanDefaultFalse("show_g_prediction"); + // } else { + // return usingG6(); + // } + } + + public static boolean usingAlt() { + return (android_wear && !Pref.getBooleanDefaultFalse("only_ever_use_wear_collector")) + || WholeHouse.isLive(); + } + + private static class OperationSuccess extends RuntimeException { + private OperationSuccess(String message) { + super(message); + UserError.Log.d(TAG, "Operation Success: " + message); + } + } + + private static byte[] nn(final byte[] array) { + if (array == null) { + if (JoH.ratelimit("never-null", 60)) { + UserError.Log.wtf("NeverNullOb1", "Attempt to pass null!!! " + JoH.backTrace()); + return new byte[1]; + } + } + return array; + } + + private static String devName() { + return usingG6() ? "G6" : "G5"; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1Work.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1Work.java new file mode 100644 index 0000000..d202f9f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Ob1Work.java @@ -0,0 +1,33 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.google.common.collect.ImmutableSet; +import com.google.gson.annotations.Expose; + +/** + * Created by jamorham on 12/10/2017. + */ + +public class Ob1Work { + + private static final ImmutableSet streamClasses = ImmutableSet.of(SessionStartTxMessage.class, SessionStopTxMessage.class, CalibrateTxMessage.class); + + @Expose + public final BaseMessage msg; + @Expose + public final String text; + @Expose + public final long timestamp; + public volatile int retry = 0; + + Ob1Work(BaseMessage msg, String text) { + this.msg = msg; + this.text = text; + this.timestamp = JoH.tsl(); + } + + public boolean streamable() { + return streamClasses.contains(msg.getClass()); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/RawScaling.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/RawScaling.java new file mode 100644 index 0000000..42cae52 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/RawScaling.java @@ -0,0 +1,36 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.usingG6; + +public class RawScaling { + + public enum DType { + G5, G6v1, G6v2 + } + + public static double scale(final long raw, final DType version, final boolean filtered) { + switch (version) { + case G6v1: + return raw * 34; + case G6v2: + return (raw - 1151500000) / 110; + default: + return raw; + } + } + + public static double scale(final long raw, final String transmitter_id, final boolean filtered) { + final boolean g6 = usingG6(); + + if (!g6) { + return scale(raw, DType.G5, filtered); + } else { + final boolean g6r2 = FirmwareCapability.isTransmitterG6Rev2(transmitter_id); + return scale(raw, g6r2 ? DType.G6v2 : DType.G6v1, filtered); + } + + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/ResetTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/ResetTxMessage.java new file mode 100644 index 0000000..063a432 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/ResetTxMessage.java @@ -0,0 +1,15 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +// jamorham + +class ResetTxMessage extends BaseMessage { + static final byte opcode = 0x42; + + ResetTxMessage() { + init(opcode, 3); + UserError.Log.d(TAG, "ResetTx dbg: " + JoH.bytesToHex(byteSequence)); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorDays.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorDays.java new file mode 100644 index 0000000..e8283e5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorDays.java @@ -0,0 +1,202 @@ +package com.eveningoutpost.dexdrip.G5Model; + + +import android.text.SpannableString; + +import com.eveningoutpost.dexdrip.Models.Sensor; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight; +import com.eveningoutpost.dexdrip.ui.helpers.Span; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.eveningoutpost.dexdrip.xdrip; + +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Locale; + +import lombok.Getter; +import lombok.val; + +import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.getFirmwareXDetails; +import static com.eveningoutpost.dexdrip.Models.JoH.msSince; +import static com.eveningoutpost.dexdrip.Models.JoH.roundDouble; +import static com.eveningoutpost.dexdrip.Models.JoH.tsl; +import static com.eveningoutpost.dexdrip.Services.G5BaseService.usingG6; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.getTransmitterID; +import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.usingNativeMode; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.DAY_IN_MS; +import static com.eveningoutpost.dexdrip.utils.DexCollectionType.getDexCollectionType; +import static com.eveningoutpost.dexdrip.utils.DexCollectionType.hasDexcomRaw; +import static com.eveningoutpost.dexdrip.utils.DexCollectionType.hasLibre; + +// jamorham + +// helper class to deal with sensor expiry + +public class SensorDays { + + private static final String TAG = "SensorDays"; + + private static final long UNKNOWN = -1; + private static final int USE_DEXCOM_STRATEGY = 5; + private static final int USE_LIBRE_STRATEGY = 6; + + private static final long CAL_THRESHOLD1 = DAY_IN_MS * 4; + private static final long CAL_THRESHOLD2 = Constants.HOUR_IN_MS * 18; + + private static final HashMap cache = new HashMap<>(); + + @Getter + private long period = UNKNOWN; + private long created = 0; + private int strategy = 0; + + // load current config and compute + public static SensorDays get() { + val type = getDexCollectionType(); + val tx_id = getTransmitterID(); + return get(type, tx_id); + } + + // compute based on type + public static SensorDays get(final DexCollectionType type, final String tx_id) { + + // get cached result + val result = cache.get(type.toString() + tx_id); + if (result != null && result.cacheValid()) return result; + + val ths = new SensorDays(); + + if (hasLibre(type)) { + ths.period = Constants.DAY_IN_MS * 14; // TODO 10 day sensors? + ths.strategy = USE_LIBRE_STRATEGY; + + } else if (hasDexcomRaw(type)) { + ths.strategy = USE_DEXCOM_STRATEGY; + val vr2 = (VersionRequest2RxMessage) + getFirmwareXDetails(tx_id, 2); + if (vr2 != null) { + ths.period = Constants.DAY_IN_MS * vr2.typicalSensorDays; + } else { + if (usingG6()) { + ths.period = Constants.DAY_IN_MS * 10; // G6 default + } else { + ths.period = Constants.DAY_IN_MS * 7; // G5 + } + } + + } else { + // unknown type + } + ths.created = tsl(); + cache.put(type.toString() + tx_id, ths); + return ths; + } + + private long getDexcomStart() { + if (usingNativeMode()) { + return DexSessionKeeper.getStart(); + } else { + try { + // In non-native mode the expiration is a guide only + return Sensor.currentSensor().started_at; + } catch (Exception e) { + return -1; + } + } + } + + private long getLibreStart() { + try { + val age_minutes = Pref.getInt("nfc_sensor_age", -50000); + if (age_minutes > 0) { + return tsl() - (age_minutes * Constants.MINUTE_IN_MS); + } else { + return Sensor.currentSensor().started_at; + } + } catch (Exception e) { + return -1; + } + } + + private long getStart() { + switch (strategy) { + case USE_DEXCOM_STRATEGY: + return getDexcomStart(); + case USE_LIBRE_STRATEGY: + return getLibreStart(); + default: + return 0; // very large error default will be caught by sanity check + } + } + + private boolean isStarted() { + return getStart() > 0; + } + + // returns 0 if invalid + long getRemainingSensorPeriodInMs() { + //UserError.Log.d(TAG, "Get start debug returns: " + JoH.dateTimeText(getStart())); + if (isValid()) { + val elapsed = msSince(getStart()); + long remaining = period - elapsed; + // sanity check + if ((remaining < 0) || (remaining > period)) { + remaining = 0; + } + return remaining; + } else { + return 0; + } + } + + long getSensorEndTimestamp() { + if (isValid()) { + return getStart() + period; + } else { + return 0; + } + } + + // Add resolution / update cache + public SpannableString getSpannable() { + + val expiryMs = getRemainingSensorPeriodInMs(); + + if (expiryMs > 0) { + if (expiryMs > CAL_THRESHOLD1) { + val fmt = xdrip.gs(R.string.expires_days); + return new SpannableString(MessageFormat.format(fmt, roundDouble((double) expiryMs / DAY_IN_MS, 1))); + } else { + // expiring soon + val niceTime = new SimpleDateFormat(expiryMs < CAL_THRESHOLD2 ? "h:mm a" : "EEE, h:mm a", Locale.getDefault()).format(getSensorEndTimestamp()); + return Span.colorSpan(MessageFormat.format(xdrip.gs(R.string.expires_at), niceTime), expiryMs < CAL_THRESHOLD2 ? Highlight.BAD.color() : Highlight.NOTICE.color()); + } + } + return new SpannableString(""); + } + + public boolean isValid() { + return isKnown() && isSessionLive() && isStarted(); + } + + private boolean isSessionLive() { + return Sensor.isActive(); + } + + boolean isKnown() { + return period != UNKNOWN; + } + + boolean cacheValid() { + return msSince(created) < Constants.MINUTE_IN_MS * 10; + } + + void invalidateCache() { + created = -1; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorRxMessage.java new file mode 100644 index 0000000..cec52b5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorRxMessage.java @@ -0,0 +1,33 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by jcostik1 on 3/26/16. + */ +public class SensorRxMessage extends BaseMessage { + public static final byte opcode = 0x2f; + public TransmitterStatus status; + public int timestamp; + public int unfiltered; + public int filtered; + private final static String TAG = G5CollectionService.TAG; // meh + + public SensorRxMessage(byte[] packet) { + UserError.Log.d(TAG, "SensorRX dbg: " + JoH.bytesToHex(packet)); + if (packet.length >= 14) { + if (packet[0] == opcode) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + + status = TransmitterStatus.getBatteryLevel(data.get(1)); + timestamp = data.getInt(2); + + unfiltered = data.getInt(6); + filtered = data.getInt(10); + } + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorTxMessage.java new file mode 100644 index 0000000..9caf60c --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SensorTxMessage.java @@ -0,0 +1,23 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by jcostik1 on 3/26/16. + */ +public class SensorTxMessage extends BaseMessage { + byte opcode = 0x2e; + byte[] crc = CRC.calculate(opcode); + + + public SensorTxMessage() { + data = ByteBuffer.allocate(3); + data.put(opcode); + data.put(crc); + byteSequence = data.array(); + UserError.Log.d(TAG, "SensorTx dbg: " + JoH.bytesToHex(byteSequence)); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartRxMessage.java new file mode 100644 index 0000000..8c8eeb4 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartRxMessage.java @@ -0,0 +1,90 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// created by jamorham + +public class SessionStartRxMessage extends BaseMessage { + public static final byte opcode = 0x27; + final byte length = 17; + + private byte status = (byte) 0xFF; + private byte info = (byte) 0xFF; + final String transmitterId; + int sessionStartTime = 0; + int requestedStartTime = 0; + int transitterTime = 0; + + boolean valid = false; + + public SessionStartRxMessage(byte[] packet, String transmitterId) { + this.transmitterId = transmitterId; + if (packet.length == length) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + valid = true; + status = data.get(); + info = data.get(); + requestedStartTime = data.getInt(); + sessionStartTime = data.getInt(); + transitterTime = data.getInt(); + } + } + } + + boolean isValid() { + return valid; + } + + boolean isOkay() { + return isValid() && status == 0x00 && (info == 0x01 || info == 0x05) && sessionStartTime != INVALID_TIME; + } + + // beyond hope? + boolean isFubar() { + return info == 0x04; + } + + long getSessionStart() { + if (isOkay() && sessionStartTime > 0) { + return DexTimeKeeper.fromDexTime(transmitterId, sessionStartTime); + } else { + return 0; + } + } + + long getRequestedStart() { + if (isOkay() && requestedStartTime > 0) { + return DexTimeKeeper.fromDexTime(transmitterId, requestedStartTime); + } else { + return 0; + } + } + + long getTransmitterTime() { + if (isOkay() && transitterTime > 0) { + return DexTimeKeeper.fromDexTime(transmitterId, transitterTime); + } else { + return 0; + } + } + + String message() { + switch (info) { + case 0x01: + return "OK"; + case 0x02: + return "Already started"; + case 0x03: + return "Invalid"; + case 0x04: + return "Clock not synchronized or other error"; // probably + case 0x05: + return "OK G6"; // probably + default: + return "Unknown code: " + info; + } + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartTxMessage.java new file mode 100644 index 0000000..90d557b --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStartTxMessage.java @@ -0,0 +1,57 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import lombok.Getter; + +// created by jamorham + +public class SessionStartTxMessage extends BaseMessage { + + final byte opcode = 0x26; + @Getter + private final long startTime; + @Getter + private final int dexTime; + + public SessionStartTxMessage(int dexTime) { + this((int) (JoH.tsl() / 1000), dexTime); + } + + public SessionStartTxMessage(long startTime, int dexTime) { + this(startTime, dexTime, null); + } + + public SessionStartTxMessage(long startTime, int dexTime, String code) { + this.startTime = startTime; + this.dexTime = dexTime; + final boolean using_g6 = (code != null); + data = ByteBuffer.allocate(code == null || new G6CalibrationParameters(code).isNullCode() ? (using_g6 ? 13 : 11) : 17); + data.order(ByteOrder.LITTLE_ENDIAN); + data.put(opcode); + data.putInt(dexTime); + data.putInt((int) (startTime / 1000)); + + if (code != null) { + final G6CalibrationParameters params = new G6CalibrationParameters(code); + if (params.isValid() && !params.isNullCode()) { + data.putShort((short) params.getParamA()); + data.putShort((short) params.getParamB()); + } else { + if (!params.isValid()) { + throw new IllegalArgumentException("Invalid G6 code in SessionStartTxMessage"); + } + } + } + if (using_g6) { + data.putShort((short) 0x0000); + } + appendCRC(); + UserError.Log.d(TAG, "SessionStartTxMessage dbg: " + JoH.bytesToHex(byteSequence)); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopRxMessage.java new file mode 100644 index 0000000..f8ae444 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopRxMessage.java @@ -0,0 +1,61 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import lombok.Getter; + +// created by jamorham + +public class SessionStopRxMessage extends BaseMessage { + + public static final byte opcode = 0x29; + final byte length = 17; + @Getter + private byte status = (byte)0xFF; + private byte received = (byte)0xFF; + final String transmitterId; + int sessionStartTime=0; + int sessionStopTime =0; + int transitterTime=0; + boolean valid = false; + + public SessionStopRxMessage(byte[] packet,String transmitterId) { + this.transmitterId = transmitterId; + if (packet.length == length) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if ((data.get() == opcode) && checkCRC(packet)) { + valid = true; + status = data.get(); + received = data.get(); + sessionStopTime = data.getInt(); + sessionStartTime = data.getInt(); + transitterTime = data.getInt(); + + } + } + } + + boolean isValid() { + return valid; + } + + boolean isOkay() { + return isValid() && status == 0x00; + } + + long getSessionStart() { + if (isOkay() && sessionStartTime > 0) { + return DexTimeKeeper.fromDexTime(transmitterId, sessionStartTime); + } else { + return 0; + } + } + long getSessionStop() { + if (isOkay() && sessionStopTime > 0) { + return DexTimeKeeper.fromDexTime(transmitterId, sessionStopTime); + } else { + return 0; + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopTxMessage.java new file mode 100644 index 0000000..9fd46c0 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/SessionStopTxMessage.java @@ -0,0 +1,30 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +// created by jamorham + +public class SessionStopTxMessage extends BaseMessage { + + final byte opcode = 0x28; + final int length = 7; + { + postExecuteGuardTime = 1000; + } + + SessionStopTxMessage(int stopTime) { + + init(opcode, length); + data.putInt(stopTime); + appendCRC(); + } + + SessionStopTxMessage(String transmitterId) { + final int stopTime = DexTimeKeeper.getDexTime(transmitterId, JoH.tsl()); + init(opcode, 7); + data.putInt(stopTime); + appendCRC(); + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TimeTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TimeTxMessage.java new file mode 100644 index 0000000..ff6f374 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TimeTxMessage.java @@ -0,0 +1,11 @@ +package com.eveningoutpost.dexdrip.G5Model; + +// jamorham + +public class TimeTxMessage extends BaseMessage { + public static final byte opcode = 0x24; + + TimeTxMessage() { + init(opcode, 3); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Transmitter.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Transmitter.java new file mode 100644 index 0000000..f7b47c1 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/Transmitter.java @@ -0,0 +1,16 @@ +package com.eveningoutpost.dexdrip.G5Model; + +/** + * Created by joeginley on 3/19/16. + */ +public class Transmitter { + public String transmitterId = ""; + + public Transmitter(String id) { + transmitterId = id; + } + + public void authenticate() { + + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterMessage.java new file mode 100644 index 0000000..211cbf2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterMessage.java @@ -0,0 +1,67 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Services.G5CollectionService; +import com.google.gson.annotations.Expose; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Created by joeginley on 3/16/16. + */ + +public class TransmitterMessage { + protected static final String TAG = G5CollectionService.TAG; // meh + static final int INVALID_TIME = 0xFFFFFFFF; + @Expose + long postExecuteGuardTime = 50; + + @Expose + public volatile byte[] byteSequence = null; + public ByteBuffer data = null; + + + public void setData() { + byte[] newData; + } + + static int getUnsignedShort(ByteBuffer data) { + return ((data.get() & 0xff) + ((data.get() & 0xff) << 8)); + } + + static int getUnsignedByte(ByteBuffer data) { + return ((data.get() & 0xff)); + } + + static int getUnixTime() { + return (int) (JoH.tsl() / 1000); + } + + void init(byte opcode, int length) { + data = ByteBuffer.allocate(length); + data.order(ByteOrder.LITTLE_ENDIAN); + data.put(opcode); + } + + + byte[] appendCRC() { + data.put(CRC.calculate(getByteSequence(), 0, byteSequence.length - 2)); + return getByteSequence(); + } + + boolean checkCRC(byte[] data) { + if ((data == null) || (data.length < 3)) return false; + final byte[] crc = CRC.calculate(data, 0, data.length - 2); + return crc[0] == data[data.length - 2] && crc[1] == data[data.length - 1]; + } + + byte[] getByteSequence() { + byteSequence = data.array(); + return byteSequence; + } + + long guardTime() { + return postExecuteGuardTime; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterStatus.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterStatus.java new file mode 100644 index 0000000..0272d86 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterStatus.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.G5Model; + +/** + * Created by joeginley on 3/28/16. + */ +public enum TransmitterStatus { + UNKNOWN, BRICKED, LOW, OK; + + public static TransmitterStatus getBatteryLevel(int b) { + if (b > 0x81) { + return BRICKED; + } + else { + if (b == 0x81) { + return LOW; + } + else if (b == 0x00) { + return OK; + } + else { + return UNKNOWN; + } + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeRxMessage.java new file mode 100644 index 0000000..ac824b2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeRxMessage.java @@ -0,0 +1,60 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import lombok.Getter; + +/** + * Created by joeginley on 3/28/16. + */ +public class TransmitterTimeRxMessage extends BaseMessage { + public static final byte opcode = 0x25; + @Getter + private TransmitterStatus status; + @Getter + private int currentTime; + @Getter + private int sessionStartTime; + + public TransmitterTimeRxMessage(byte[] packet) { + if (packet.length >= 10) { + if (packet[0] == opcode && checkCRC(packet)) { + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + + status = TransmitterStatus.getBatteryLevel(data.get(1)); + currentTime = data.getInt(2); + sessionStartTime = data.getInt(6); + // TODO more bytes after this? + } + } + } + + public boolean sessionInProgress() { + return sessionStartTime != -1 && currentTime != sessionStartTime; + } + + public long getRealSessionStartTime(long now) { + return now - ((currentTime - sessionStartTime) * 1000); + } + + public long getRealSessionStartTime() { + + if (sessionInProgress()) { + return getRealSessionStartTime(JoH.tsl()); + } else { + return -1; + } + } + + public long getSessionDuration() { + if (sessionInProgress()) { + return JoH.msSince(getRealSessionStartTime()); + } else { + return -1; + } + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeTxMessage.java new file mode 100644 index 0000000..9f11c37 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/TransmitterTimeTxMessage.java @@ -0,0 +1,18 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; + +/** + * Created by joeginley on 3/28/16. + */ +public class TransmitterTimeTxMessage extends TransmitterMessage { + public static final byte opcode = 0x24; + private final static byte[] crc = CRC.calculate(opcode); + + public TransmitterTimeTxMessage() { + data = ByteBuffer.allocate(3); + data.put(opcode); + data.put(crc); + byteSequence = data.array(); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/UnbondRequestTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/UnbondRequestTxMessage.java new file mode 100644 index 0000000..e7470a8 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/UnbondRequestTxMessage.java @@ -0,0 +1,17 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; + +/** + * Created by joeginley on 3/16/16. + */ +public class UnbondRequestTxMessage extends TransmitterMessage { + byte opcode = 0x6; + + public UnbondRequestTxMessage() { + data = ByteBuffer.allocate(1); + data.put(opcode); + + byteSequence = data.array(); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest1RxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest1RxMessage.java new file mode 100644 index 0000000..48c2bbf --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest1RxMessage.java @@ -0,0 +1,46 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Created by jamorham on 25/11/2016. + */ + +public class VersionRequest1RxMessage extends BaseMessage { + + public static final byte opcode = 0x4B; + + public int status; + public String firmware_version_string; + public long build_version; + public int version_code; + public int inactive_days; + public int max_inactive_days; + public int max_runtime_days; + + + public VersionRequest1RxMessage(byte[] packet) { + if (packet.length >= 18) { + // TODO check CRC?? + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if (data.get() == opcode) { + status = data.get(); + firmware_version_string = dottedStringFromData(data, 4); + build_version = getUnsignedInt(data); + inactive_days = getUnsignedShort(data); + version_code = getUnsignedByte(data); + max_runtime_days = getUnsignedShort(data); + max_inactive_days = getUnsignedShort(data); + // crc + } + } + } + + public String toString() { + return String.format(Locale.US, "Status: %s / FW version: %s / Version Code: %d / Build: %d / Inactive: %d / Max Inactive: %d / Max Runtime: %d", + TransmitterStatus.getBatteryLevel(status).toString(), firmware_version_string, version_code, build_version, inactive_days, max_inactive_days, max_runtime_days); + } + +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest2RxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest2RxMessage.java new file mode 100644 index 0000000..2063310 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequest2RxMessage.java @@ -0,0 +1,40 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Created by jamorham on 25/11/2016. + */ + + +public class VersionRequest2RxMessage extends BaseMessage { + + public static final byte opcode = 0x53; + + public int status; + public int typicalSensorDays; + public int featureBits; + + + public VersionRequest2RxMessage(byte[] packet) { + if (packet.length >= 18) { + // TODO check CRC?? + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if (data.get() == opcode) { + status = data.get(); + typicalSensorDays = getUnsignedByte(data); + featureBits = getUnsignedShort(data); + // 12 more bytes of unknown data + // crc + } + } + } + + public String toString() { + return String.format(Locale.US, "Status: %s / Typical Days: %d / : Feature Bits %d", + TransmitterStatus.getBatteryLevel(status).toString(), typicalSensorDays, featureBits); + } + +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestRxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestRxMessage.java new file mode 100644 index 0000000..30f7ecd --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestRxMessage.java @@ -0,0 +1,44 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Created by jamorham on 25/11/2016. + */ + + +public class VersionRequestRxMessage extends BaseMessage { + + public static final byte opcode = 0x21; + + public int status; + public String firmware_version_string; + public String bluetooth_firmware_version_string; + public int hardwarev; + public String other_firmware_version; + public int asic; + + + public VersionRequestRxMessage(byte[] packet) { + if (packet.length >= 18) { + // TODO check CRC?? + data = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN); + if (data.get() == opcode) { + status = data.get(); + firmware_version_string = dottedStringFromData(data, 4); + bluetooth_firmware_version_string = dottedStringFromData(data, 4); + hardwarev = data.get(); + other_firmware_version = dottedStringFromData(data, 3); + asic = getUnsignedShort(data); // check signed vs unsigned & byte order!! + } + } + } + + public String toString() { + return String.format(Locale.US, "Status: %s / Firmware: %s / BT-Firmware: %s / Other-FW: %s / hardwareV: %d / asic: %d", + TransmitterStatus.getBatteryLevel(status).toString(), firmware_version_string, bluetooth_firmware_version_string, other_firmware_version, hardwarev, asic); + } + +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestTxMessage.java b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestTxMessage.java new file mode 100644 index 0000000..a8c791e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/G5Model/VersionRequestTxMessage.java @@ -0,0 +1,38 @@ +package com.eveningoutpost.dexdrip.G5Model; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError; + +/** + * Created by jamorham on 25/11/2016. + */ + +public class VersionRequestTxMessage extends BaseMessage { + + static final byte opcode0 = 0x20; + static final byte opcode1 = 0x4A; + static final byte opcode2 = 0x52; + + public VersionRequestTxMessage() { + this(0); + } + + public VersionRequestTxMessage(final int version) { + byte this_opcode = 0; + switch (version) { + case 0: + this_opcode = opcode0; + break; + case 1: + this_opcode = opcode1; + break; + case 2: + this_opcode = opcode2; + break; + + } + init(this_opcode, 3); + UserError.Log.d(TAG, "VersionTx (" + version + ") dbg: " + JoH.bytesToHex(byteSequence)); + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRC16.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRC16.java new file mode 100644 index 0000000..ec5092c --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRC16.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class CRC16 { + public static byte[] calculate(byte[] buff, int start, int end) { + int crcShort = 0; + for (int i = start; i < end; i++) { + crcShort = ((crcShort >>> 8) | (crcShort << 8) )& 0xffff; + crcShort ^= (buff[i] & 0xff); + crcShort ^= ((crcShort & 0xff) >> 4); + crcShort ^= (crcShort << 12) & 0xffff; + crcShort ^= ((crcShort & 0xFF) << 5) & 0xffff; + } + crcShort &= 0xffff; + return new byte[] {(byte) (crcShort & 0xff), (byte) ((crcShort >> 8) & 0xff)}; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRCFailRuntimeException.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRCFailRuntimeException.java new file mode 100644 index 0000000..9f7fdb1 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/CRCFailRuntimeException.java @@ -0,0 +1,11 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class CRCFailRuntimeException extends RuntimeException { + public CRCFailRuntimeException(String message){ + super(message); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Dex_Constants.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Dex_Constants.java new file mode 100644 index 0000000..b659635 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Dex_Constants.java @@ -0,0 +1,210 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class Dex_Constants { + + public final static int NULL = 0; + public final static int ACK = 1; + public final static int NAK = 2; + public final static int INVALID_COMMAND = 3; + public final static int INVALID_PARAM = 4; + public final static int INCOMPLETE_PACKET_RECEIVED = 5; + public final static int RECEIVER_ERROR = 6; + public final static int INVALID_MODE = 7; + public final static int PING = 10; + public final static int READ_FIRMWARE_HEADER = 11; + public final static int READ_DATABASE_PARTITION_INFO = 15; + public final static int READ_DATABASE_PAGE_RANGE = 16; + public final static int READ_DATABASE_PAGES = 17; + public final static int READ_DATABASE_PAGE_HEADER = 18; + public final static int READ_TRANSMITTER_ID = 25; + public final static int WRITE_TRANSMITTER_ID = 26; + public final static int READ_LANGUAGE = 27; + public final static int WRITE_LANGUAGE = 28; + public final static int READ_DISPLAY_TIME_OFFSET = 29; + public final static int WRITE_DISPLAY_TIME_OFFSET = 30; + public final static int READ_RTC = 31; + public final static int RESET_RECEIVER = 32; + public final static int READ_BATTERY_LEVEL = 33; + public final static int READ_SYSTEM_TIME = 34; + public final static int READ_SYSTEM_TIME_OFFSET = 35; + public final static int WRITE_SYSTEM_TIME = 36; + public final static int READ_GLUCOSE_UNIT = 37; + public final static int WRITE_GLUCOSE_UNIT = 38; + public final static int READ_BLINDED_MODE = 39; + public final static int WRITE_BLINDED_MODE = 40; + public final static int READ_CLOCK_MODE = 41; + public final static int WRITE_CLOCK_MODE = 42; + public final static int READ_DEVICE_MODE = 43; + public final static int ERASE_DATABASE = 45; + public final static int SHUTDOWN_RECEIVER = 46; + public final static int WRITE_PC_PARAMETERS = 47; + public final static int READ_BATTERY_STATE = 48; + public final static int READ_HARDWARE_BOARD_ID = 49; + public final static int READ_FIRMWARE_SETTINGS = 54; + public final static int READ_ENABLE_SETUP_WIZARD_FLAG = 55; + public final static int READ_SETUP_WIZARD_STATE = 57; + public final static int MAX_COMMAND = 59; + public final static int MAX_POSSIBLE_COMMAND = 255; + public final static int EGV_VALUE_MASK = 1023; + public final static int EGV_DISPLAY_ONLY_MASK = 32768; + public final static int EGV_TREND_ARROW_MASK = 15; + public final static int EGV_NOISE_MASK = 112; + public final static float MG_DL_TO_MMOL_L = 0.05556f; + public final static int CRC_LEN = 2; + public static final int TRANSMITTER_BATTERY_LOW = 210; + public static final int TRANSMITTER_BATTERY_EMPTY = 207; + + public enum BATTERY_STATES { + NONE, + CHARGING, + NOT_CHARGING, + NTC_FAULT, + BAD_BATTERY + } + + public enum RECORD_TYPES { + MANUFACTURING_DATA, + FIRMWARE_PARAMETER_DATA, + PC_SOFTWARE_PARAMETER, + SENSOR_DATA, + EGV_DATA, + CAL_SET, + DEVIATION, + INSERTION_TIME, + RECEIVER_LOG_DATA, + RECEIVER_ERROR_DATA, + METER_DATA, + USER_EVENT_DATA, + USER_SETTING_DATA, + MAX_VALUE + } + + public enum TREND_ARROW_VALUES { + NONE(0), + DOUBLE_UP(1,"\u21C8", "DoubleUp"), + SINGLE_UP(2,"\u2191", "SingleUp"), + UP_45(3,"\u2197", "FortyFiveUp"), + FLAT(4,"\u2192", "Flat"), + DOWN_45(5,"\u2198", "FortyFiveDown"), + SINGLE_DOWN(6,"\u2193", "SingleDown"), + DOUBLE_DOWN(7,"\u21CA", "DoubleDown"), + NOT_COMPUTABLE(8, "", "NOT_COMPUTABLE"), + OUT_OF_RANGE(9, "", "OUT_OF_RANGE"); + + private String arrowSymbol; + private String trendName; + private int myID; + + TREND_ARROW_VALUES(int id, String a, String t) { + myID=id; + arrowSymbol = a; + trendName = t; + } + + TREND_ARROW_VALUES(int id) { + this(id,null, null); + } + + public String Symbol() { + if (arrowSymbol == null) { + return "\u2194"; + } else { + return arrowSymbol; + } + } + + public String friendlyTrendName() { + if (trendName == null) { + return this.name().replace("_", " "); + } else { + return this.trendName; + } + } + + public int getID(){ + return myID; + } + + } + + public enum SPECIALBGVALUES_MGDL { + NONE("??0", 0), + SENSORNOTACTIVE("?SN", 1), + MINIMALLYEGVAB("??2", 2), + NOANTENNA("?NA", 3), + SENSOROUTOFCAL("?NC", 5), + COUNTSAB("?CD", 6), + ABSOLUTEAB("?AD", 9), + POWERAB("???", 10), + RFBADSTATUS("?RF", 12); + + + private String name; + private int val; + private SPECIALBGVALUES_MGDL(String s, int i){ + name=s; + val=i; + } + + public int getValue(){ + return val; + } + + public String toString(){ + return name; + } + + public static SPECIALBGVALUES_MGDL getEGVSpecialValue(int val){ + for (SPECIALBGVALUES_MGDL e: values()){ + if (e.getValue()==val) + return e; + } + return null; + } + + public static boolean isSpecialValue(int val){ + for (SPECIALBGVALUES_MGDL e: values()){ + if (e.getValue()==val) + return true; + } + return false; + } + + } + + public enum InsertionState { + NONE, + REMOVED, + EXPIRED, + RESIDUAL_DEVIATION, + COUNTS_DEVIATION, + SECOND_SESSION, + OFF_TIME_LOSS, + STARTED, + BAD_TRANSMITTER, + MANUFACTURING_MODE, + MAX_VALUE + } + + public enum NOISE { + NOISE_NONE(0), + CLEAN(1), + LIGHT(2), + MEDIUM(3), + HEAVY(4), + NOT_COMPUTED(5), + MAX(6); + + private final int value; + + private NOISE(int value) { + this.value = value; + } + + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/PacketBuilder.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/PacketBuilder.java new file mode 100644 index 0000000..72134cf --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/PacketBuilder.java @@ -0,0 +1,108 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import java.util.ArrayList; +import java.util.List; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class PacketBuilder { + public static final int MAX_PAYLOAD = 1584; + public static final int MIN_LEN = 6; + public static final int MAX_LEN = MAX_PAYLOAD + MIN_LEN; + public static final byte SOF = 0x01; + public static final int OFFSET_SOF = 0; + public static final int OFFSET_LENGTH = 1; + public static final int OFFSET_NULL = 2; + public static final byte NULL = 0x00; + public static final int OFFSET_CMD = 3; + public static final int OFFSET_PAYLOAD = 4; + public static final int CRC_LEN = 2; + public static final int HEADER_LEN = 4; + public ArrayList packet; + public int command; + public ArrayList payload; + + public PacketBuilder(int command) { + this.command = command; + } + + public PacketBuilder(int command, ArrayList payload) { + this.command = command; + this.payload = payload; + } + + public byte[] compose() { + packet = new ArrayList(); + packet.add(OFFSET_SOF, SOF); + packet.add(OFFSET_LENGTH, getLength()); + packet.add(OFFSET_NULL, NULL); + packet.add(OFFSET_CMD, (byte) command); + if (this.payload != null) { this.packet.addAll(OFFSET_PAYLOAD, this.payload); } + byte[] crc16 = CRC16.calculate(toBytes(), 0, this.packet.size()); + this.packet.add(crc16[0]); + this.packet.add(crc16[1]); + Log.d("ShareTest", "About to start adding to Byte, size: " + this.packet.size()); + return this.toBytes(); + } + + public List composeList() { + packet = new ArrayList(); + packet.add(OFFSET_SOF, SOF); + packet.add(OFFSET_LENGTH, getLength()); + packet.add(OFFSET_NULL, NULL); + packet.add(OFFSET_CMD, (byte) command); + if (this.payload != null) { this.packet.addAll(OFFSET_PAYLOAD, this.payload); } + byte[] crc16 = CRC16.calculate(toBytes(), 0, this.packet.size()); + this.packet.add(crc16[0]); + this.packet.add(crc16[1]); + Log.d("ShareTest", "About to start adding to ByteList, size: " + this.packet.size()); + return this.toBytesList(); + } + + private byte getLength() { + int packetSize = payload == null ? MIN_LEN : payload.size() + CRC_LEN + HEADER_LEN; + + if (packetSize > MAX_LEN) { + throw new IndexOutOfBoundsException(packetSize + " bytes, but packet must between " + + MIN_LEN + " and " + MAX_LEN + " bytes."); + } + + return (byte) packetSize; + } + + public byte[] toBytes() { + byte[] b = new byte[this.packet.size()]; + for (int i = 0; i < this.packet.size(); i++) { + b[i] = this.packet.get(i).byteValue(); + } + return b; + } + + public List toBytesList() { + List byteMessages = new ArrayList(); + double totalPacketSize = packet.size(); + int messages =(int) Math.ceil(totalPacketSize/18); + for(int m = 0; m < messages; m++) { + int thisPacketSize; + if (m == messages - 1) { + thisPacketSize = ((this.packet.size()+2) % 18); + } else { + thisPacketSize = (20); + } + int offset = m * 18; + Log.d("ShareTest", "This packet size: " + thisPacketSize); + byte[] b = new byte[thisPacketSize]; + b[0] = (byte) (m + 1); + b[1] = (byte) (messages); + for (int i = 2; i < thisPacketSize; i++) { + b[i] = packet.get(offset + i - 2).byteValue(); + } + byteMessages.add(b); + } + return byteMessages; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadData.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadData.java new file mode 100644 index 0000000..c49e6b2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadData.java @@ -0,0 +1,348 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.GenericXMLRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.MeterRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.PageHeader; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.driver.UsbSerialDriver; +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import org.w3c.dom.Element; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +public class ReadData { + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + + private static final String TAG = ReadData.class.getSimpleName(); + private static final int IO_TIMEOUT = 3000; + private static final int MIN_LEN = 256; + private UsbSerialDriver mSerialDevice; + protected final Object mReadBufferLock = new Object(); + private UsbDeviceConnection mConnection; + private UsbDevice mDevice; + + public ReadData(){} + public ReadData(UsbSerialDriver device) { + mSerialDevice = device; + } + public ReadData(UsbSerialDriver device, UsbDeviceConnection connection, UsbDevice usbDevice) { + mSerialDevice = device; + mConnection = connection; + mDevice = usbDevice; + try { + mSerialDevice.getPorts().get(0).open(connection); + } catch(IOException e) { + Log.d("FAILED WHILE", "trying to open"); + } +// } + } + + public EGVRecord[] getRecentEGVs() { + int recordType = Dex_Constants.RECORD_TYPES.EGV_DATA.ordinal(); + int endPage = readDataBasePageRange(recordType); + return readDataBasePage(recordType, endPage); + } + + public EGVRecord[] getRecentEGVsPages(int numOfRecentPages) { + if (numOfRecentPages < 1) { + throw new IllegalArgumentException("Number of pages must be greater than 1."); + } + Log.d(TAG, "Reading EGV page range..."); + int recordType = Dex_Constants.RECORD_TYPES.EGV_DATA.ordinal(); + int endPage = readDataBasePageRange(recordType); + Log.d(TAG, "Reading " + numOfRecentPages + " EGV page(s)..."); + numOfRecentPages = numOfRecentPages - 1; + EGVRecord[] allPages = new EGVRecord[0]; + for (int i = Math.min(numOfRecentPages,endPage); i >= 0; i--) { + int nextPage = endPage - i; + Log.d(TAG, "Reading #" + i + " EGV pages (page number " + nextPage + ")"); + EGVRecord[] ithEGVRecordPage = readDataBasePage(recordType, nextPage); + EGVRecord[] result = Arrays.copyOf(allPages, allPages.length + ithEGVRecordPage.length); + System.arraycopy(ithEGVRecordPage, 0, result, allPages.length, ithEGVRecordPage.length); + allPages = result; + } + Log.d(TAG, "Read complete of EGV pages."); + return allPages; + } + + public long getTimeSinceEGVRecord(EGVRecord egvRecord) { + return readSystemTime() - egvRecord.getSystemTimeSeconds(); + } + + public MeterRecord[] getRecentMeterRecords() { + Log.d(TAG, "Reading Meter page..."); + int recordType = Dex_Constants.RECORD_TYPES.METER_DATA.ordinal(); + int endPage = readDataBasePageRange(recordType); + return readDataBasePage(recordType, endPage); + } + + public SensorRecord[] getRecentSensorRecords(int numOfRecentPages) { + if (numOfRecentPages < 1) { + throw new IllegalArgumentException("Number of pages must be greater than 1."); + } + Log.d(TAG, "Reading Sensor page range..."); + int recordType = Dex_Constants.RECORD_TYPES.SENSOR_DATA.ordinal(); + int endPage = readDataBasePageRange(recordType); + Log.d(TAG, "Reading " + numOfRecentPages + " Sensor page(s)..."); + numOfRecentPages = numOfRecentPages - 1; + SensorRecord[] allPages = new SensorRecord[0]; + for (int i = Math.min(numOfRecentPages,endPage); i >= 0; i--) { + int nextPage = endPage - i; + Log.d(TAG, "Reading #" + i + " Sensor pages (page number " + nextPage + ")"); + SensorRecord[] ithSensorRecordPage = readDataBasePage(recordType, nextPage); + SensorRecord[] result = Arrays.copyOf(allPages, allPages.length + ithSensorRecordPage.length); + System.arraycopy(ithSensorRecordPage, 0, result, allPages.length, ithSensorRecordPage.length); + allPages = result; + } + Log.d(TAG, "Read complete of Sensor pages."); + return allPages; + } + + public CalRecord[] getRecentCalRecords() { + Log.d(TAG, "Reading Cal Records page range..."); + int recordType = Dex_Constants.RECORD_TYPES.CAL_SET.ordinal(); + int endPage = readDataBasePageRange(recordType); + Log.d(TAG, "Reading Cal Records page..."); + return readDataBasePage(recordType, endPage); + } + public byte[] getRecentCalRecordsTest() { + Log.d(TAG, "Reading Cal Records page range..."); + int recordType = Dex_Constants.RECORD_TYPES.CAL_SET.ordinal(); + int endPage = readDataBasePageRange(recordType); + Log.d(TAG, "Reading Cal Records page..."); + return readDataBasePageTest(recordType, endPage); + } + + public boolean ping() { + writeCommand(Dex_Constants.PING); + return read(MIN_LEN).getCommand() == Dex_Constants.ACK; + } + + public int readBatteryLevel() { + Log.d(TAG, "Reading battery level..."); + writeCommand(Dex_Constants.READ_BATTERY_LEVEL); + byte[] readData = read(MIN_LEN).getData(); + return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + public String readSerialNumber() { + int PAGE_OFFSET = 0; + byte[] readData = readDataBasePage(Dex_Constants.RECORD_TYPES.MANUFACTURING_DATA.ordinal(), PAGE_OFFSET); + Element md = ParsePage(readData, Dex_Constants.RECORD_TYPES.MANUFACTURING_DATA.ordinal()); + return md.getAttribute("SerialNumber"); + } + + public Date readDisplayTime() { + return Utils.receiverTimeToDate(readSystemTime() + readDisplayTimeOffset()); + } + + public long readSystemTime() { + Log.d(TAG, "Reading system time..."); + writeCommand(Dex_Constants.READ_SYSTEM_TIME); + byte[] readData = read(MIN_LEN).getData(); + return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt() & 0xffffffff; + } + + public int readDisplayTimeOffset() { + Log.d(TAG, "Reading display time offset..."); + writeCommand(Dex_Constants.READ_DISPLAY_TIME_OFFSET); + byte[] readData = read(MIN_LEN).getData(); + return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt() & 0xffffffff; + } + + private int readDataBasePageRange(int recordType) { + ArrayList payload = new ArrayList(); + Log.d(TAG, "adding Payload"); + payload.add((byte) recordType); + Log.d(TAG, "Sending write command"); + writeCommand(Dex_Constants.READ_DATABASE_PAGE_RANGE, payload); + Log.d(TAG, "About to call getdata"); + byte[] readData = read(MIN_LEN).getData(); + Log.d(TAG, "Going to return"); + return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(4); + } + + private T readDataBasePage(int recordType, int page) { + byte numOfPages = 1; + if (page < 0){ + throw new IllegalArgumentException("Invalid page requested:" + page); + } + ArrayList payload = new ArrayList(); + payload.add((byte) recordType); + byte[] pageInt = ByteBuffer.allocate(4).putInt(page).array(); + payload.add(pageInt[3]); + payload.add(pageInt[2]); + payload.add(pageInt[1]); + payload.add(pageInt[0]); + payload.add(numOfPages); + writeCommand(Dex_Constants.READ_DATABASE_PAGES, payload); + byte[] readData = read(2122).getData(); + return ParsePage(readData, recordType); + } + private byte[] readDataBasePageTest(int recordType, int page) { + byte numOfPages = 1; + if (page < 0){ + throw new IllegalArgumentException("Invalid page requested:" + page); + } + ArrayList payload = new ArrayList(); + payload.add((byte) recordType); + byte[] pageInt = ByteBuffer.allocate(4).putInt(page).array(); + payload.add(pageInt[3]); + payload.add(pageInt[2]); + payload.add(pageInt[1]); + payload.add(pageInt[0]); + payload.add(numOfPages); + return writeCommandTest(Dex_Constants.READ_DATABASE_PAGES, payload); + } + + private void writeCommand(int command, ArrayList payload) { + byte[] packet = new PacketBuilder(command, payload).compose(); + if (mSerialDevice != null) { + try { +// UsbInterface mDataInterface = mDevice.getInterface(1); +// UsbEndpoint mWriteEndpoint = mDataInterface.getEndpoint(0); +// mConnection.bulkTransfer(mWriteEndpoint, packet, packet.length, IO_TIMEOUT); + mSerialDevice.getPorts().get(0).write(packet, IO_TIMEOUT); + } catch (Exception e) { + Log.e(TAG, "Unable to write to serial device.", e); + } + } + } + private byte[] writeCommandTest(int command, ArrayList payload) { + byte[] packet = new PacketBuilder(command, payload).compose(); + return packet; + } + private void writeCommand(int command) { + byte[] packet = new PacketBuilder(command).compose(); + if (mSerialDevice != null) { + try { +// UsbInterface mDataInterface = mDevice.getInterface(1); +// UsbEndpoint mWriteEndpoint = mDataInterface.getEndpoint(0); +// mConnection.bulkTransfer(mWriteEndpoint, packet, packet.length, IO_TIMEOUT); + mSerialDevice.getPorts().get(0).write(packet, IO_TIMEOUT); + } catch (Exception e) { + Log.e(TAG, "Unable to write to serial device.", e); + } + } + } + + private ReadPacket read(int numOfBytes) { + byte[] readData = new byte[numOfBytes]; + int len = 0; + try { +// UsbInterface mDataInterface = mDevice.getInterface(1); +// UsbEndpoint mReadEndpoint = mDataInterface.getEndpoint(1); +// byte[] mReadBuffer; +// mReadBuffer = new byte[16 * 1024]; +// +// int readAmt = Math.min(readData.length, mReadBuffer.length); +// synchronized (mReadBufferLock) { +// +// +// Log.d(TAG, "Read about to call bulk transfer."); +// if (len < 0) { +// // This sucks: we get -1 on timeout, not 0 as preferred. +// // We *should* use UsbRequest, except it has a bug/api oversight +// // where there is no way to determine the number of bytes read +// // in response :\ -- http://b.android.com/28023 +// if (IO_TIMEOUT == Integer.MAX_VALUE) { +// // Hack: Special case "~infinite timeout" as an error. +// len = -1; +// } +// len = 0; +// } +// +//// System.arraycopy(mReadBuffer, 0, readData, 0, readAmt); +// } +// len = mConnection.bulkTransfer(mReadEndpoint, readData, readAmt, IO_TIMEOUT); + + len = mSerialDevice.getPorts().get(0).read(readData, IO_TIMEOUT); + + Log.d(TAG, "Read " + len + " byte(s) complete."); + + // Add a 100ms delay for when multiple write/reads are occurring in series + Thread.sleep(100); + + // TODO: this debug code to print data of the read, should be removed after + // finding the source of the reading issue + String bytes = ""; + int readAmount = len; + for (int i = 0; i < readAmount; i++) bytes += String.format("%02x", readData[i]) + " "; + Log.d(TAG, "Read data: " + bytes); + //////////////////////////////////////////////////////////////////////////////////////// + + } catch (Exception e) { + Log.e(TAG, "Unable to read from serial device.", e); + } + byte[] data = Arrays.copyOfRange(readData, 0, len); + return new ReadPacket(data); + } + + private T ParsePage(byte[] data, int recordType) { + int HEADER_LEN = 28; + PageHeader pageHeader=new PageHeader(data); + int NUM_REC_OFFSET = 4; + int numRec = data[NUM_REC_OFFSET]; + int rec_len; + + switch (Dex_Constants.RECORD_TYPES.values()[recordType]) { + case MANUFACTURING_DATA: + GenericXMLRecord xmlRecord = new GenericXMLRecord(Arrays.copyOfRange(data, HEADER_LEN, data.length - 1)); + return (T) xmlRecord; + case SENSOR_DATA: + rec_len = 20; + SensorRecord[] sensorRecords = new SensorRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + sensorRecords[i] = new SensorRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + return (T) sensorRecords; + case EGV_DATA: + rec_len = 13; + EGVRecord[] egvRecords = new EGVRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + egvRecords[i] = new EGVRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + return (T) egvRecords; + case METER_DATA: + rec_len = 16; + MeterRecord[] meterRecords = new MeterRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + meterRecords[i] = new MeterRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + return (T) meterRecords; + case CAL_SET: + rec_len = 249; + if (pageHeader.getRevision()<=2) { + rec_len = 148; + } + CalRecord[] calRecords = new CalRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + calRecords[i] = new CalRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + return (T) calRecords; + default: + // Throw error "Database record not supported" + break; + } + + return (T) null; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadDataShare.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadDataShare.java new file mode 100644 index 0000000..2305645 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadDataShare.java @@ -0,0 +1,305 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.GenericXMLRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.MeterRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.PageHeader; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; +import com.eveningoutpost.dexdrip.Services.DexShareCollectionService; +import com.eveningoutpost.dexdrip.ShareTest; + +import org.w3c.dom.Element; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import rx.Observable; +import rx.functions.Action1; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class ReadDataShare { + byte[] accumulatedResponse; + private ShareTest mShareTest; + private DexShareCollectionService mCollectionService; + + public ReadDataShare(ShareTest aShareTest){ + mShareTest = aShareTest; + } + public ReadDataShare(DexShareCollectionService collectionService){ + mCollectionService = collectionService; + } + + public void getRecentEGVs(final Action1 recordListener) { + final int recordType = Dex_Constants.RECORD_TYPES.EGV_DATA.ordinal(); + final Action1 fullPageListener = new Action1() { + @Override + public void call(byte[] s) { ParsePage(read(0,s).getData(), recordType, recordListener); } + }; + Action1 databasePageRangeCaller = new Action1() { + @Override + public void call(Integer s) { readDataBasePage(recordType, s, fullPageListener); } + }; + readDataBasePageRange(recordType, databasePageRangeCaller); + } + + public void getRecentMeterRecords(final Action1 recordListener) { + final int recordType = Dex_Constants.RECORD_TYPES.METER_DATA.ordinal(); + final Action1 fullPageListener = new Action1() { + @Override + public void call(byte[] s) { ParsePage(read(0,s).getData(), recordType, recordListener); } + }; + Action1 databasePageRangeCaller = new Action1() { + @Override + public void call(Integer s) { readDataBasePage(recordType, s, fullPageListener); } + }; + readDataBasePageRange(recordType, databasePageRangeCaller); + } + + public void getRecentCalRecords(final Action1 recordListener) { + final int recordType = Dex_Constants.RECORD_TYPES.CAL_SET.ordinal(); + final Action1 fullPageListener = new Action1() { + @Override + public void call(byte[] s) { ParsePage(read(0,s).getData(), recordType, recordListener); } + }; + Action1 databasePageRangeCaller = new Action1() { + @Override + public void call(Integer s) { readDataBasePage(recordType, s, fullPageListener); } + }; + readDataBasePageRange(recordType, databasePageRangeCaller); + } + + + public void getRecentSensorRecords(final Action1 recordListener) { + final int recordType = Dex_Constants.RECORD_TYPES.SENSOR_DATA.ordinal(); + final Action1 fullPageListener = new Action1() { + @Override + public void call(byte[] s) { ParsePage(read(0,s).getData(), recordType, recordListener); } + }; + Action1 databasePageRangeCaller = new Action1() { + @Override + public void call(Integer s) { readDataBasePage(recordType, s, fullPageListener); } + }; + readDataBasePageRange(recordType, databasePageRangeCaller); + } + + public void getTimeSinceEGVRecord(final EGVRecord egvRecord, final Action1 timeSinceEgvRecord) { + Action1 tempSystemTimeListener = new Action1() { + @Override + public void call(Long s) { Observable.just(s - egvRecord.getSystemTimeSeconds()).subscribe(timeSinceEgvRecord); } + }; + readSystemTime(tempSystemTimeListener); + } + + public void ping(final Action1 pingListener) { + Action1 pingReader = new Action1() { + @Override + public void call(byte[] s) { Observable.just(read(0, s).getCommand() == Dex_Constants.ACK).subscribe(pingListener); } + }; + writeCommand(Dex_Constants.PING, pingReader); + } + + public void readBatteryLevel(final Action1 batteryLevelListener) { + Action1 batteryLevelReader = new Action1() { + @Override //TODO: find out if this should be wrapped in read(s).getData(); + public void call(byte[] s) { Observable.just(ByteBuffer.wrap(s).order(ByteOrder.LITTLE_ENDIAN).getInt()).subscribe(batteryLevelListener); } + }; + writeCommand(Dex_Constants.READ_BATTERY_LEVEL, batteryLevelReader); + } + + public void readSerialNumber(final Action1 serialNumberListener) { + final Action1 manufacturingDataListener = new Action1() { + @Override + public void call(byte[] s) { + Element el = ParsePage(s, Dex_Constants.RECORD_TYPES.MANUFACTURING_DATA.ordinal()); + Observable.just(el.getAttribute("SerialNumber")).subscribe(serialNumberListener); + } + }; + readDataBasePage(Dex_Constants.RECORD_TYPES.MANUFACTURING_DATA.ordinal(), 0, manufacturingDataListener); + } + + public void readDisplayTime(final Action1 displayTimeListener) { + Action1 tempSystemTimeListener = new Action1() { + @Override + public void call(Long s) { + final long systemTime = s; + Action1 tempSystemTimeListener = new Action1() { + @Override + public void call(Long s) { + Date dateDisplayTime = Utils.receiverTimeToDate(systemTime + s); + Observable.just(dateDisplayTime).subscribe(displayTimeListener); } + }; + readDisplayTimeOffset(tempSystemTimeListener); + } + }; + readSystemTime(tempSystemTimeListener); + } + + public void readSystemTime(final Action1 systemTimeListener) { + Action1 systemTimeReader = new Action1() { + @Override + public void call(byte[] s) { + Observable.just(Utils.receiverTimeToDate(ByteBuffer.wrap(read(0,s).getData()).order(ByteOrder.LITTLE_ENDIAN).getInt()).getTime()).subscribe(systemTimeListener); + } + }; + writeCommand(Dex_Constants.READ_SYSTEM_TIME, systemTimeReader); + } + + public void readDisplayTimeOffset(final Action1 displayTimeOffsetListener) { + Action1 displayTimeOffsetReader = new Action1() { + @Override + public void call(byte[] s) { Observable.just((long) ByteBuffer.wrap(read(0,s).getData()).order(ByteOrder.LITTLE_ENDIAN).getInt()).subscribe(displayTimeOffsetListener); } + }; + writeCommand(Dex_Constants.READ_DISPLAY_TIME_OFFSET, displayTimeOffsetReader); + } + + private void readDataBasePageRange(int recordType, final Action1 databasePageRangeCaller) { + ArrayList payload = new ArrayList(); + payload.add((byte) recordType); + final Action1 databasePageRangeListener = new Action1() { + @Override + public void call(byte[] s) { + Observable.just(ByteBuffer.wrap(new ReadPacket(s).getData()).order(ByteOrder.LITTLE_ENDIAN).getInt(4)).subscribe(databasePageRangeCaller); + } + }; + writeCommand(Dex_Constants.READ_DATABASE_PAGE_RANGE, payload, databasePageRangeListener); + } + + private T readDataBasePage(final int recordType, int page, final Action1 fullPageListener) { + byte numOfPages = 1; + if (page < 0){ throw new IllegalArgumentException("Invalid page requested:" + page); } + ArrayList payload = new ArrayList(); + payload.add((byte) recordType); + byte[] pageInt = ByteBuffer.allocate(4).putInt(page).array(); + payload.add(pageInt[3]); + payload.add(pageInt[2]); + payload.add(pageInt[1]); + payload.add(pageInt[0]); + payload.add(numOfPages); + accumulatedResponse = null; + final Action1 databasePageReader = new Action1() { + @Override + public void call(byte[] s) { + Log.d("ShareTest", "Database Page Reader received SIZE: " + s.length); + byte[] temp = s; + if (accumulatedResponse == null) { + accumulatedResponse = s; + } else { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(accumulatedResponse); + outputStream.write(temp); + accumulatedResponse = outputStream.toByteArray(); + Log.d("ShareTest", "Combined Response length: " + accumulatedResponse.length); + } catch (Exception e) { e.printStackTrace(); } + } + if (temp.length < 20) { Observable.just(accumulatedResponse).subscribe(fullPageListener).unsubscribe(); } + } + }; + writeCommand(Dex_Constants.READ_DATABASE_PAGES, payload, databasePageReader); + return null; + } + + private void writeCommand(int command, ArrayList payload, Action1 responseListener) { + List packets = new PacketBuilder(command, payload).composeList(); + if(mShareTest != null) { mShareTest.writeCommand(packets, 0, responseListener); } + else if (mCollectionService != null) { mCollectionService.writeCommand(packets, 0, responseListener); } + } + + private void writeCommand(int command, Action1 responseListener) { + List packets = new PacketBuilder(command).composeList(); + if(mShareTest != null) { mShareTest.writeCommand(packets, 0, responseListener); } + else if (mCollectionService != null) { mCollectionService.writeCommand(packets, 0, responseListener); } + } + + private ReadPacket read(int numOfBytes, byte[] readPacket) { + return new ReadPacket(Arrays.copyOfRange(readPacket, 0, readPacket.length)); + } + + private T ParsePage(byte[] data, int recordType) { return ParsePage(data, recordType, null); } + private T ParsePage(byte[] data, int recordType, Action1 parsedPageReceiver) { + int HEADER_LEN = 28; + PageHeader pageHeader=new PageHeader(data); + int NUM_REC_OFFSET = 4; + int numRec = data[NUM_REC_OFFSET]; + int rec_len; + + switch (Dex_Constants.RECORD_TYPES.values()[recordType]) { + case MANUFACTURING_DATA: + GenericXMLRecord xmlRecord = new GenericXMLRecord(Arrays.copyOfRange(data, HEADER_LEN, data.length - 1)); + if(parsedPageReceiver != null) { + Observable.just((T) xmlRecord).subscribe(parsedPageReceiver); + } else { + return (T) xmlRecord; + } + break; + case SENSOR_DATA: + rec_len = 20; + SensorRecord[] sensorRecords = new SensorRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + sensorRecords[i] = new SensorRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + if(parsedPageReceiver != null) { + Observable.just((T) sensorRecords).subscribe(parsedPageReceiver); + } else { + return (T) sensorRecords; + } + break; + case EGV_DATA: + rec_len = 13; + EGVRecord[] egvRecords = new EGVRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + egvRecords[i] = new EGVRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + if(parsedPageReceiver != null) { + Observable.just((T) egvRecords).subscribe(parsedPageReceiver); + } else { + return (T) egvRecords; + } + break; + case METER_DATA: + rec_len = 16; + MeterRecord[] meterRecords = new MeterRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + meterRecords[i] = new MeterRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + if(parsedPageReceiver != null) { + Observable.just((T) meterRecords).subscribe(parsedPageReceiver); + } else { + return (T) meterRecords; + } + break; + case CAL_SET: + rec_len = 249; + if (pageHeader.getRevision()<=2) { rec_len = 148; } + CalRecord[] calRecords = new CalRecord[numRec]; + for (int i = 0; i < numRec; i++) { + int startIdx = HEADER_LEN + rec_len * i; + calRecords[i] = new CalRecord(Arrays.copyOfRange(data, startIdx, startIdx + rec_len - 1)); + } + if(parsedPageReceiver != null) { + Observable.just((T) calRecords).subscribe(parsedPageReceiver); + } else { + return (T) calRecords; + } + break; + default: + break; + } + Observable.just((T) null).subscribe(parsedPageReceiver); + return (T) null; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadPacket.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadPacket.java new file mode 100644 index 0000000..f84f20c --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/ReadPacket.java @@ -0,0 +1,36 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import java.util.Arrays; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + + +public class ReadPacket { + private int command; + private byte[] data; + private byte[] crc_calc; + private byte[] crc; + private int OFFSET_CMD = 3; + private int OFFSET_DATA = 4; + private int CRC_LEN = 2; + + public ReadPacket(byte[] readPacket) { + this.command = readPacket[OFFSET_CMD]; + this.data = Arrays.copyOfRange(readPacket, OFFSET_DATA, readPacket.length - CRC_LEN); + this.crc = Arrays.copyOfRange(readPacket, readPacket.length - CRC_LEN, readPacket.length); + this.crc_calc=CRC16.calculate(readPacket, 0, readPacket.length - 2); + if (!Arrays.equals(this.crc, this.crc_calc)) { + throw new CRCFailRuntimeException("CRC check failed: " + Utils.bytesToHex(this.crc) + " vs " + Utils.bytesToHex(this.crc_calc)); + } + } + + public int getCommand() { + return command; + } + + public byte[] getData() { + return data; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/SyncingService.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/SyncingService.java new file mode 100644 index 0000000..c06b48a --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/SyncingService.java @@ -0,0 +1,346 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.os.PowerManager; +import android.preference.PreferenceManager; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.GlucoseDataSet; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.MeterRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.driver.CdcAcmSerialDriver; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.driver.ProbeTable; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.driver.UsbSerialDriver; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.driver.UsbSerialProber; +import com.eveningoutpost.dexdrip.Models.Calibration; + +import org.json.JSONArray; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + + +/** + * An {@link IntentService} subclass for handling asynchronous CGM Receiver downloads and cloud uploads + * requests in a service on a separate handler thread. + */ +public class SyncingService extends IntentService { + + // Action for intent + private static final String ACTION_SYNC = "com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.action.SYNC"; + private static final String ACTION_CALIBRATION_CHECKIN = "com.eveningoutpost.dexdrip.CalibrationCheckInActivity"; + + // Parameters for intent + private static final String SYNC_PERIOD = "com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.extra.SYNC_PERIOD"; + + // Response to broadcast to activity + public static final String RESPONSE_SGV = "mySGV"; + public static final String RESPONSE_TREND = "myTrend"; + public static final String RESPONSE_TIMESTAMP = "myTimestamp"; + public static final String RESPONSE_NEXT_UPLOAD_TIME = "myUploadTime"; + public static final String RESPONSE_UPLOAD_STATUS = "myUploadStatus"; + public static final String RESPONSE_DISPLAY_TIME = "myDisplayTime"; + public static final String RESPONSE_JSON = "myJSON"; + public static final String RESPONSE_BAT = "myBatLvl"; + + private final String TAG = SyncingService.class.getSimpleName(); + private Context mContext; + private UsbManager mUsbManager; + private UsbSerialDriver mSerialDevice; + private UsbDevice dexcom; + private UsbDeviceConnection mConnection; + + // Constants + private final int TIME_SYNC_OFFSET = 10000; + public static final int MIN_SYNC_PAGES = 2; + public static final int GAP_SYNC_PAGES = 20; + + + /** + * Starts this service to perform action Single Sync with the given parameters. If + * the service is already performing a task this action will be queued. + * + * @see IntentService + */ + public static void startActionSingleSync(Context context, int numOfPages) { + Intent intent = new Intent(context, SyncingService.class); + intent.setAction(ACTION_SYNC); + intent.putExtra(SYNC_PERIOD, numOfPages); + context.startService(intent); + } + public static void startActionCalibrationCheckin(Context context) { + Intent intent = new Intent(context, SyncingService.class); + intent.setAction(ACTION_CALIBRATION_CHECKIN); + context.startService(intent); + } + public SyncingService() { + super("SyncingService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + mContext = getApplicationContext(); + if (intent != null) { + final String action = intent.getAction(); + if (ACTION_SYNC.equals(action)) { + final int param1 = intent.getIntExtra(SYNC_PERIOD, 1); + handleActionSync(param1); + } else if (ACTION_CALIBRATION_CHECKIN.equals(action)) { + Log.i("CALIBRATION-CHECK-IN: ", "Beginning check in process"); + performCalibrationCheckin(); + } + } + } + + /** + * Handle action Sync in the provided background thread with the provided + * parameters. + */ + private void performCalibrationCheckin(){ + PowerManager pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "NSDownload"); + wl.acquire(); + try { + Log.i("CALIBRATION-CHECK-IN: ", "Wake Lock Acquired"); + if (acquireSerialDevice()) { + try { + ReadData readData = new ReadData(mSerialDevice, mConnection, dexcom); + +// ReadData readData = new ReadData(mSerialDevice); + CalRecord[] calRecords = readData.getRecentCalRecords(); + Log.i("CALIBRATION-CHECK-IN: ", "Found " + calRecords.length + " Records!"); + save_most_recent_cal_record(calRecords); + + } catch (Exception e) { + Log.wtf("Unhandled exception caught", e); + } finally { + // Close serial + try { + mSerialDevice.getPorts().get(0).close(); + } catch (IOException e) { + Log.e(TAG, "Unable to close", e); + } + + } + } else { + Log.w("CALIBRATION-CHECK-IN: ", "Failed to acquire serial device"); + } + } finally { + JoH.releaseWakeLock(wl); + } + } + + private void handleActionSync(int numOfPages) { + boolean broadcastSent = false; + + PowerManager pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "NSDownload"); + wl.acquire(); + + try { + sync(numOfPages); + } finally { + wl.release(); + } + } + + private void sync(int numOfPages) { + boolean broadcastSent; + if (acquireSerialDevice()) { + try { + + ReadData readData = new ReadData(mSerialDevice); + // TODO: need to check if numOfPages if valid on ReadData side + EGVRecord[] recentRecords = readData.getRecentEGVsPages(numOfPages); + MeterRecord[] meterRecords = readData.getRecentMeterRecords(); + // TODO: need to check if numOfPages if valid on ReadData side + SensorRecord[] sensorRecords = readData.getRecentSensorRecords(numOfPages); + GlucoseDataSet[] glucoseDataSets = Utils.mergeGlucoseDataRecords(recentRecords, sensorRecords); + + // FIXME: This is a workaround for the new Dexcom AP which seems to have a new format + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + CalRecord[] calRecords = new CalRecord[1]; + if (prefs.getBoolean("cloud_cal_data", false)) { + calRecords = readData.getRecentCalRecords(); + } + + long timeSinceLastRecord = readData.getTimeSinceEGVRecord(recentRecords[recentRecords.length - 1]); + // TODO: determine if the logic here is correct. I suspect it assumes the last record was less than 5 + // minutes ago. If a reading is skipped and the device is plugged in then nextUploadTime will be + // set to a negative number. This situation will eventually correct itself. + long nextUploadTime = (1000 * 60 * 5) - (timeSinceLastRecord * (1000)); + long displayTime = readData.readDisplayTime().getTime(); + // FIXME: Device seems to flake out on battery level reads. Removing for now. +// int batLevel = readData.readBatteryLevel(); + int batLevel = 100; + + // convert into json for d3 plot + JSONArray array = new JSONArray(); + for (int i = 0; i < recentRecords.length; i++) array.put(recentRecords[i].toJSON()); + + EGVRecord recentEGV = recentRecords[recentRecords.length - 1]; +// broadcastSGVToUI(recentEGV, uploadStatus, nextUploadTime + TIME_SYNC_OFFSET, +// displayTime, array ,batLevel); + broadcastSent=true; + } catch (ArrayIndexOutOfBoundsException e) { + Log.wtf("Unable to read from the dexcom, maybe it will work next time", e); + } catch (NegativeArraySizeException e) { + Log.wtf("Negative array exception from receiver", e); + } catch (IndexOutOfBoundsException e) { + Log.wtf("IndexOutOfBounds exception from receiver", e); + } catch (CRCFailRuntimeException e){ + // FIXME: may consider localizing this catch at a lower level (like ReadData) so that + // if the CRC check fails on one type of record we can capture the values if it + // doesn't fail on other types of records. This means we'd need to broadcast back + // partial results to the UI. Adding it to a lower level could make the ReadData class + // more difficult to maintain - needs discussion. + Log.wtf("CRC failed", e); + } catch (Exception e) { + Log.wtf("Unhandled exception caught", e); + } finally { + // Close serial + try { + mSerialDevice.getPorts().get(0).close(); + } catch (IOException e) { + + Log.e(TAG, "Unable to close", e); + } + + } + } + // if (!broadcastSent) broadcastSGVToUI(); + } + + private void save_most_recent_cal_record(CalRecord[] calRecords) { + int size = calRecords.length; + Calibration.create(calRecords,getApplicationContext(), false, 0); + } + + private boolean acquireSerialDevice() { + UsbDevice found_device = findDexcom(); + + if (mUsbManager == null) { + Log.w("CALIBRATION-CHECK-IN: ", "USB manager is null"); + return false; + } + + if (dexcom == null) { + Log.e(TAG, "dex device == null"); + return false; + } + + if( mUsbManager.hasPermission(dexcom)) { // the system is allowing us to poke around this device + + ProbeTable customTable = new ProbeTable(); // From the USB library... + customTable.addProduct(0x22A3, 0x0047, CdcAcmSerialDriver.class); // ...Specify the Vendor ID and Product ID + + UsbSerialProber prober = new UsbSerialProber(customTable); // Probe the device with the custom values + List drivers = prober.findAllDrivers(mUsbManager); // let's go through the list + Iterator foo = drivers.iterator(); // Invalid Return code + while (foo.hasNext()) { // let's loop through + UsbSerialDriver driver = foo.next(); // set fooDriver to the next available driver + if (driver != null) { + UsbDeviceConnection connection = mUsbManager.openDevice(driver.getDevice()); + if (connection != null) { + mSerialDevice = driver; + + mConnection = connection; + Log.i("CALIBRATION-CHECK-IN: ", "CONNECTEDDDD!!"); + return true; + } + } else { + Log.w("CALIBRATION-CHECK-IN: ", "Driver was no good"); + } + } + Log.w("CALIBRATION-CHECK-IN: ", "No usable drivers found"); + } else { + Log.w("CALIBRATION-CHECK-IN: ", "You dont have permissions for that dexcom!!"); + } + return false; + } + + static public boolean isG4Connected(Context c){ + UsbManager manager = (UsbManager) c.getSystemService(Context.USB_SERVICE); + HashMap deviceList = manager.getDeviceList(); + Log.i("USB DEVICES = ", deviceList.toString()); + Iterator deviceIterator = deviceList.values().iterator(); + Log.i("USB DEVICES = ", String.valueOf(deviceList.size())); + + while(deviceIterator.hasNext()){ + UsbDevice device = deviceIterator.next(); + if (device.getVendorId() == 8867 && device.getProductId() == 71 + && device.getDeviceClass() == 2 && device.getDeviceSubclass() ==0 + && device.getDeviceProtocol() == 0){ + Log.i("CALIBRATION-CHECK-IN: ", "Dexcom Found!"); + return true; + } + } + return false; + } + + public UsbDevice findDexcom() { + Log.i("CALIBRATION-CHECK-IN: ", "Searching for dexcom"); + mUsbManager = (UsbManager) getApplicationContext().getSystemService(Context.USB_SERVICE); + Log.i("USB MANAGER = ", mUsbManager.toString()); + HashMap deviceList = mUsbManager.getDeviceList(); + Log.i("USB DEVICES = ", deviceList.toString()); + Iterator deviceIterator = deviceList.values().iterator(); + Log.i("USB DEVICES = ", String.valueOf(deviceList.size())); + + while(deviceIterator.hasNext()){ + UsbDevice device = deviceIterator.next(); + if (device.getVendorId() == 8867 && device.getProductId() == 71 + && device.getDeviceClass() == 2 && device.getDeviceSubclass() ==0 + && device.getDeviceProtocol() == 0){ + dexcom = device; + Log.i("CALIBRATION-CHECK-IN: ", "Dexcom Found!"); + return device; + } else { + Log.w("CALIBRATION-CHECK-IN: ", "that was not a dexcom (I dont think)"); + } + } + return null; + } + + private void broadcastSGVToUI(EGVRecord egvRecord, boolean uploadStatus, + long nextUploadTime, long displayTime, + JSONArray json, int batLvl) { + Log.d(TAG, "Current EGV: " + egvRecord.getBGValue()); + Intent broadcastIntent = new Intent(); +// broadcastIntent.setAction(MainActivity.CGMStatusReceiver.PROCESS_RESPONSE); + broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT); + broadcastIntent.putExtra(RESPONSE_SGV, egvRecord.getBGValue()); + broadcastIntent.putExtra(RESPONSE_TREND, egvRecord.getTrend().getID()); + broadcastIntent.putExtra(RESPONSE_TIMESTAMP, egvRecord.getDisplayTime().getTime()); + broadcastIntent.putExtra(RESPONSE_NEXT_UPLOAD_TIME, nextUploadTime); + broadcastIntent.putExtra(RESPONSE_UPLOAD_STATUS, uploadStatus); + broadcastIntent.putExtra(RESPONSE_DISPLAY_TIME, displayTime); + if (json!=null) + broadcastIntent.putExtra(RESPONSE_JSON, json.toString()); + broadcastIntent.putExtra(RESPONSE_BAT, batLvl); + sendBroadcast(broadcastIntent); + } + + private void broadcastSGVToUI() { + EGVRecord record=new EGVRecord(-1, Dex_Constants.TREND_ARROW_VALUES.NONE,new Date(),new Date()); + broadcastSGVToUI(record,false, (long) (1000 * 60 * 5) + TIME_SYNC_OFFSET, new Date().getTime(), null, 0); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Utils.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Utils.java new file mode 100644 index 0000000..ca69853 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/Utils.java @@ -0,0 +1,75 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.GlucoseDataSet; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; + +import java.util.Date; +import java.util.TimeZone; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class Utils { + + public static Date receiverTimeToDate(long delta) { + int currentTZOffset = TimeZone.getDefault().getRawOffset(); + long epochMS = 1230768000000L; // Jan 01, 2009 00:00 in UTC + long milliseconds = epochMS - currentTZOffset; + long timeAdd = milliseconds + (1000L * delta); + TimeZone tz = TimeZone.getDefault(); + if (tz.inDaylightTime(new Date())) timeAdd = timeAdd - (1000 * 60 * 60); + return new Date(timeAdd); + } + + public static String getTimeString(long timeDeltaMS) { + long minutes = (timeDeltaMS / 1000) / 60; + long hours = minutes / 60; + long days = hours / 24; + long weeks = days / 7; + minutes= minutes - hours * 60; + hours = hours - days * 24; + days= days - weeks * 7; + + String timeAgoString = ""; + if (weeks > 0) { + timeAgoString += weeks + " weeks "; + } + if (days > 0) { + timeAgoString += days + " days "; + } + if (hours > 0) { + timeAgoString += hours + " hours "; + } + if (minutes >= 0) { + timeAgoString += minutes + " min "; + } + + return (timeAgoString.equals("") ? "--" : timeAgoString + "ago"); + } + + public static GlucoseDataSet[] mergeGlucoseDataRecords(EGVRecord[] egvRecords, + SensorRecord[] sensorRecords) { + int egvLength = egvRecords.length; + int sensorLength = sensorRecords.length; + int smallerLength = egvLength < sensorLength ? egvLength : sensorLength; + GlucoseDataSet[] glucoseDataSets = new GlucoseDataSet[smallerLength]; + for (int i = 1; i <= smallerLength; i++) { + glucoseDataSets[smallerLength - i] = new GlucoseDataSet(egvRecords[egvLength - i], sensorRecords[sensorLength - i]); + } + return glucoseDataSets; + } + + public static String bytesToHex(byte[] bytes) { + char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 3]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 3] = hexArray[v >>> 4]; + hexChars[j * 3 + 1] = hexArray[v & 0x0F]; + hexChars[j * 3 + 2] = " ".toCharArray()[0]; + } + return new String(hexChars); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalRecord.java new file mode 100644 index 0000000..8353cea --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalRecord.java @@ -0,0 +1,72 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class CalRecord extends GenericTimestampRecord { + private static final String TAG = CalRecord.class.getSimpleName(); + private double slope; + private double intercept; + private double scale; + private int[] unk = new int[3]; + private double decay; + private int numRecords; + private CalSubrecord[] calSubrecords = new CalSubrecord[12]; + private int SUB_LEN = 17; + + public CalRecord(byte[] packet) { + super(packet); + slope = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getDouble(8); + intercept = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getDouble(16); + scale = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getDouble(24); + unk[0] = packet[32]; + unk[1] = packet[33]; + unk[2] = packet[34]; + decay = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getDouble(35); + numRecords = packet[43]; + long displayTimeOffset = (getDisplayTime().getTime() - getSystemTime().getTime()) / (1000); + int start = 44; + for (int i = 0; i < numRecords; i++) { + Log.d("CalDebug","Loop #"+i); + byte[] temp = new byte[SUB_LEN]; + System.arraycopy(packet, start, temp, 0, temp.length); + calSubrecords[i] = new CalSubrecord(temp, displayTimeOffset); + start += SUB_LEN; + } + + Log.d("ShareTest", "slope: " + slope + " intercept: " + intercept); + } + + public double getSlope() { + return slope; + } + + public double getIntercept() { + return intercept; + } + + public double getScale() { + return scale; + } + + public int[] getUnk() { + return unk; + } + + public double getDecay() { + return decay; + } + + public int getNumRecords() { + return numRecords; + } + + public CalSubrecord[] getCalSubrecords() { + return calSubrecords; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalSubrecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalSubrecord.java new file mode 100644 index 0000000..be2f954 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/CalSubrecord.java @@ -0,0 +1,52 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class CalSubrecord { + private static final String TAG = CalSubrecord.class.getSimpleName(); + private Date dateEntered; + private int calBGL; + private int calRaw; + private Date dateApplied; + private byte unk; + + public CalSubrecord(byte[] packet, long displayTimeOffset) { + int delta = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(); + dateEntered = Utils.receiverTimeToDate(delta + displayTimeOffset); + calBGL = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(4); + calRaw = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(8); + delta = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(12); + dateApplied = Utils.receiverTimeToDate(delta + displayTimeOffset); + unk = packet[16]; + } + + public Date getDateEntered() { + return dateEntered; + } + + public int getCalBGL() { + return calBGL; + } + + public int getCalRaw() { + return calRaw; + } + + public Date getDateApplied() { + return dateApplied; + } + + public byte getUnk() { + return unk; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/EGVRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/EGVRecord.java new file mode 100644 index 0000000..6d2c378 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/EGVRecord.java @@ -0,0 +1,58 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Dex_Constants; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class EGVRecord extends GenericTimestampRecord { + + private int bGValue; + private int noise; + private Dex_Constants.TREND_ARROW_VALUES trend; + + public EGVRecord(byte[] packet) { + // system_time (UInt), display_time (UInt), glucose (UShort), trend_arrow (Byte), crc (UShort)) + super(packet); + bGValue = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getShort(8) & Dex_Constants.EGV_VALUE_MASK; + byte trendAndNoise = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).get(10); + int trendValue = trendAndNoise & Dex_Constants.EGV_TREND_ARROW_MASK; + byte noiseValue = (byte) ((trendAndNoise & Dex_Constants.EGV_NOISE_MASK) >> 4); + trend = Dex_Constants.TREND_ARROW_VALUES.values()[trendValue]; + noise = noiseValue; + } + + public EGVRecord(int bGValue, Dex_Constants.TREND_ARROW_VALUES trend, Date displayTime, Date systemTime){ + super(displayTime, systemTime); + this.bGValue=bGValue; + this.trend=trend; + } + + public String noiseValue() { return String.valueOf(noise); } + public int getBGValue() { + return bGValue; + } + + public Dex_Constants.TREND_ARROW_VALUES getTrend() { + return trend; + } + + public JSONObject toJSON() { + JSONObject obj = new JSONObject(); + try { + obj.put("sgv", getBGValue()); + obj.put("date", getDisplayTimeSeconds()); + } catch (JSONException e) { + e.printStackTrace(); + } + return obj; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericTimestampRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericTimestampRecord.java new file mode 100644 index 0000000..d1b09d4 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericTimestampRecord.java @@ -0,0 +1,48 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class GenericTimestampRecord { + + protected final int OFFSET_SYS_TIME = 0; + protected final int OFFSET_DISPLAY_TIME = 4; + protected Date systemTime; + protected int systemTimeSeconds; + protected Date displayTime; + + public GenericTimestampRecord(byte[] packet) { + systemTimeSeconds = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(OFFSET_SYS_TIME); + systemTime = Utils.receiverTimeToDate(systemTimeSeconds); + int dt = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(OFFSET_DISPLAY_TIME); + displayTime = Utils.receiverTimeToDate(dt); + } + + public GenericTimestampRecord(Date displayTime, Date systemTime){ + this.displayTime=displayTime; + this.systemTime=systemTime; + } + + public Date getSystemTime() { + return systemTime; + } + + public int getSystemTimeSeconds() { + return systemTimeSeconds; + } + + public Date getDisplayTime() { + return displayTime; + } + public long getDisplayTimeSeconds() { + return displayTime.getTime(); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericXMLRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericXMLRecord.java new file mode 100644 index 0000000..73137ac --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GenericXMLRecord.java @@ -0,0 +1,48 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.Serializable; +import java.io.StringReader; +import java.util.Arrays; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class GenericXMLRecord extends GenericTimestampRecord { + int XML_START = 8; + int XML_END = 241; + + private final String TAG = GenericXMLRecord.class.getSimpleName(); + + private Element xmlElement; + + public GenericXMLRecord(byte[] packet) { + super(packet); + Document document; + // TODO: it would be best if we could just remove /x00 characters and read till end + String xml = new String(Arrays.copyOfRange(packet, XML_START, XML_END)); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder; + try + { + builder = factory.newDocumentBuilder(); + document = builder.parse(new InputSource(new StringReader(xml))); + xmlElement = document.getDocumentElement(); + } catch (Exception e) { + Log.e(TAG, "Unable to build xml element", e); + } + } + + // example: String sn = getXmlElement().getAttribute("SerialNumber"); + public Element getXmlElement() { + return xmlElement; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GlucoseDataSet.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GlucoseDataSet.java new file mode 100644 index 0000000..13bbbb3 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/GlucoseDataSet.java @@ -0,0 +1,63 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Dex_Constants; + +import java.util.Date; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class GlucoseDataSet { + + private Date systemTime; + private Date displayTime; + private int bGValue; + private Dex_Constants.TREND_ARROW_VALUES trend; + private long unfiltered; + private long filtered; + private int rssi; + + public GlucoseDataSet(EGVRecord egvRecord, SensorRecord sensorRecord) { + // TODO check times match between record + systemTime = egvRecord.getSystemTime(); + displayTime = egvRecord.getDisplayTime(); + bGValue = egvRecord.getBGValue(); + trend = egvRecord.getTrend(); + unfiltered = sensorRecord.getUnfiltered(); + filtered = sensorRecord.getFiltered(); + rssi = sensorRecord.getRSSI(); + } + + public Date getSystemTime() { + return systemTime; + } + + public Date getDisplayTime() { + return displayTime; + } + + public int getBGValue() { + return bGValue; + } + + public Dex_Constants.TREND_ARROW_VALUES getTrend() { + return trend; + } + + public String getTrendSymbol() { + return trend.Symbol(); + } + + public long getUnfiltered() { + return unfiltered; + } + + public long getFiltered() { + return filtered; + } + + public int getRssi() { + return rssi; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/MeterRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/MeterRecord.java new file mode 100644 index 0000000..3187923 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/MeterRecord.java @@ -0,0 +1,28 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class MeterRecord extends GenericTimestampRecord { + + private int meterBG; + private int meterTime; + + public MeterRecord(byte[] packet) { + super(packet); + meterBG = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getShort(8); + meterTime = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(10); + } + + public int getMeterBG() { + return meterBG; + } + + public int getMeterTime() { + return meterTime; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/PageHeader.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/PageHeader.java new file mode 100644 index 0000000..a831d4e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/PageHeader.java @@ -0,0 +1,89 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.CRC16; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.CRCFailRuntimeException; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Dex_Constants; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class PageHeader { + protected final int HEADER_SIZE=28; + protected final int FIRSTRECORDINDEX_OFFSET=0; + protected final int NUMRECS_OFFSET=4; + protected final int RECTYPE_OFFSET=8; + protected final int REV_OFFSET=9; + protected final int PAGENUMBER_OFFSET=10; + protected final int RESERVED2_OFFSET=14; + protected final int RESERVED3_OFFSET=18; + protected final int RESERVED4_OFFSET=22; + + protected int firstRecordIndex; + protected int numOfRecords; + protected Dex_Constants.RECORD_TYPES recordType; + protected byte revision; + protected int pageNumber; + protected int reserved2; + protected int reserved3; + protected int reserved4; + protected byte[] crc=new byte[2]; + + + public PageHeader(byte[] packet) { + Log.d("ShareTest", "Header Packet Data Length: " + packet.length); + + firstRecordIndex = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(FIRSTRECORDINDEX_OFFSET); + numOfRecords = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(NUMRECS_OFFSET); + recordType = Dex_Constants.RECORD_TYPES.values()[packet[RECTYPE_OFFSET]]; + revision = packet[REV_OFFSET]; + pageNumber = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(PAGENUMBER_OFFSET); + reserved2 = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(RESERVED2_OFFSET); + reserved3 = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(RESERVED3_OFFSET); + reserved4 = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(RESERVED4_OFFSET); + System.arraycopy(packet,HEADER_SIZE- Dex_Constants.CRC_LEN,crc,0, Dex_Constants.CRC_LEN); + byte[] crc_calc = CRC16.calculate(packet,0,HEADER_SIZE - Dex_Constants.CRC_LEN); + if (!Arrays.equals(this.crc, crc_calc)) { + throw new CRCFailRuntimeException("CRC check failed: " + Utils.bytesToHex(this.crc) + " vs " + Utils.bytesToHex(crc_calc)); + } + } + + public byte getRevision() { + return revision; + } + + public Dex_Constants.RECORD_TYPES getRecordType() { + return recordType; + } + + public int getFirstRecordIndex() { + return firstRecordIndex; + } + + public int getNumOfRecords() { + return numOfRecords; + } + + public int getPageNumber() { + return pageNumber; + } + + public int getReserved2() { + return reserved2; + } + + public int getReserved3() { + return reserved3; + } + + public int getReserved4() { + return reserved4; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/SensorRecord.java b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/SensorRecord.java new file mode 100644 index 0000000..033ad6f --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/ImportedLibraries/dexcom/records/SensorRecord.java @@ -0,0 +1,42 @@ +package com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records; + +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// This code and this particular library are from the NightScout android uploader +// Check them out here: https://github.com/nightscout/android-uploader +// Some of this code may have been modified for use in this project + +public class SensorRecord extends GenericTimestampRecord { + + private int unfiltered; + private int filtered; + private int rssi; + private int OFFSET_UNFILTERED = 8; + private int OFFSET_FILTERED = 12; + private int OFFSET_RSSI = 16; + + public SensorRecord(byte[] packet) { + super(packet); + unfiltered = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(OFFSET_UNFILTERED); + filtered = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getInt(OFFSET_FILTERED); + byte [] usRSSI = new byte[]{packet[17],packet[16]}; + rssi = usRSSI[0] << 8 | usRSSI[1]; + //rssi = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).getShort(OFFSET_RSSI); + Log.d("ShareTest", "filtered: " + filtered + " unfiltered: " + unfiltered); + } + + public long getUnfiltered() { + return unfiltered; + } + + public long getFiltered() { + return filtered; + } + + public int getRSSI() { + return rssi; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/APStatus.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/APStatus.java new file mode 100644 index 0000000..4304fc7 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/APStatus.java @@ -0,0 +1,151 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.wearintegration.ExternalStatusService; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +/** + * Created by jamorham on 11/06/2018. + */ + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "APStatus", id = BaseColumns._ID) +public class APStatus extends PlusModel { + + private static boolean patched = false; + private final static String TAG = APStatus.class.getSimpleName(); + private final static boolean d = false; + + private static final String[] schema = { + "CREATE TABLE APStatus (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE APStatus ADD COLUMN timestamp INTEGER;", + "ALTER TABLE APStatus ADD COLUMN basal_percent INTEGER;", + "CREATE UNIQUE INDEX index_APStatus_timestamp on APStatus(timestamp);"}; + + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "basal_percent") + public int basal_percent; + + + public String toS() { + return JoH.defaultGsonInstance().toJson(this); + } + + // static methods + + public static APStatus createEfficientRecord(long timestamp_ms, int basal_percent) { + final APStatus existing = last(); + if (existing == null || (existing.basal_percent != basal_percent)) { + + if (existing != null && existing.timestamp > timestamp_ms) { + UserError.Log.e(TAG, "Refusing to create record older than current: " + JoH.dateTimeText(timestamp_ms) + " vs " + JoH.dateTimeText(existing.timestamp)); + return null; + } + + final APStatus fresh = APStatus.builder() + .timestamp(timestamp_ms) + .basal_percent(basal_percent) + .build(); + + UserError.Log.d(TAG, "New record created: " + fresh.toS()); + + fresh.save(); + return fresh; + } else { + return existing; + } + + } + + // TODO use persistent store? + public static APStatus last() { + try { + return new Select() + .from(APStatus.class) + .orderBy("timestamp desc") + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + updateDB(); + return null; + } + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + final List results = new Select() + .from(APStatus.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + // extend line to now if we have current data but it is continuation of last record + // so not generating a new efficient record. + if (results != null && (results.size() > 0)) { + final APStatus last = results.get(results.size() - 1); + final long last_raw_record_timestamp = ExternalStatusService.getLastStatusLineTime(); + // check are not already using the latest. + if (last_raw_record_timestamp > last.timestamp) { + final Integer last_recorded_tbr = ExternalStatusService.getTBRInt(); + if (last_recorded_tbr != null) { + if ((last.basal_percent == last_recorded_tbr) + && (JoH.msSince(last.timestamp) < Constants.HOUR_IN_MS * 3) + && (JoH.msSince(ExternalStatusService.getLastStatusLineTime()) < Constants.MINUTE_IN_MS * 20)) { + results.add(new APStatus(JoH.tsl(), last_recorded_tbr)); + UserError.Log.d(TAG, "Adding extension record"); + } + } + } + } + return results; + } catch (android.database.sqlite.SQLiteException e) { + updateDB(); + return new ArrayList<>(); + } + } + + + public static List cleanup(int retention_days) { + return new Delete() + .from(APStatus.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L)) + .execute(); + } + + + // create the table ourselves without worrying about model versioning and downgrading + public static void updateDB() { + patched = fixUpTable(schema, patched); + } +} + + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Accuracy.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Accuracy.java new file mode 100644 index 0000000..84aa199 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Accuracy.java @@ -0,0 +1,193 @@ +package com.eveningoutpost.dexdrip.Models; + +/** + * Created by jamorham on 01/02/2017. + */ + + +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.BestGlucose; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Table(name = "Accuracy", id = BaseColumns._ID) +public class Accuracy extends PlusModel { + private static final String TAG = "Accuracy"; + private static boolean patched = false; + static final String[] schema = { + "CREATE TABLE Accuracy (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE Accuracy ADD COLUMN timestamp INTEGER;", + "ALTER TABLE Accuracy ADD COLUMN bg REAL;", + "ALTER TABLE Accuracy ADD COLUMN bgtimestamp INTEGER;", + "ALTER TABLE Accuracy ADD COLUMN bgsource TEXT;", + "ALTER TABLE Accuracy ADD COLUMN plugin TEXT;", + "ALTER TABLE Accuracy ADD COLUMN calculated REAL;", + "ALTER TABLE Accuracy ADD COLUMN lag INTEGER;", + "ALTER TABLE Accuracy ADD COLUMN difference REAL;", + "CREATE INDEX index_Accuracy_timestamp on Accuracy(timestamp);", + "CREATE INDEX index_Accuracy_bgtimestamp on Accuracy(bgtimestamp);" + }; + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + @Expose + @Column(name = "bg") + public double bg; + + @Expose + @Column(name = "bgtimestamp", index = true) + public long bgtimestamp; + + @Expose + @Column(name = "bgsource") + public String bgsource; + + @Expose + @Column(name = "plugin") + public String plugin; + + @Expose + @Column(name = "calculated") + public double calculated; + + @Expose + @Column(name = "lag") + public boolean lag; + + @Expose + @Column(name = "difference") + public double difference; + + private static final boolean d = false; + + public static Accuracy create(BloodTest bloodTest, BestGlucose.DisplayGlucose dg) { + if (dg == null) return null; + final BgReading from_dg = new BgReading(); + from_dg.timestamp = dg.timestamp; + from_dg.calculated_value = dg.mgdl; + return create(bloodTest, from_dg, dg.plugin_name); + } + + + public static Accuracy create(BloodTest bloodTest, BgReading bgReading, String plugin) { + if ((bloodTest == null) || (bgReading == null)) return null; + patched = fixUpTable(schema, patched); + if (getForPreciseTimestamp(bgReading.timestamp, Constants.MINUTE_IN_MS, plugin) != null) { + UserError.Log.d(TAG, "Duplicate accuracy timestamp for: " + JoH.dateTimeText(bgReading.timestamp)); + return null; + } + final Accuracy ac = new Accuracy(); + ac.timestamp = bgReading.timestamp; + ac.bg = bloodTest.mgdl; + ac.bgtimestamp = bloodTest.timestamp; + ac.bgsource = bloodTest.source; + ac.plugin = plugin; + ac.calculated = bgReading.calculated_value; + //ac.lag = bgReading.timestamp-bloodTest.timestamp; + ac.difference = bgReading.calculated_value - bloodTest.mgdl; + ac.save(); + return ac; + } + + static Accuracy getForPreciseTimestamp(double timestamp, double precision, String plugin) { + patched = fixUpTable(schema, patched); + final Accuracy accuracy = new Select() + .from(Accuracy.class) + .where("timestamp <= ?", (timestamp + precision)) + .where("timestamp >= ?", (timestamp - precision)) + .where("plugin = ?", plugin) + .orderBy("abs(timestamp - " + timestamp + ") asc") + .executeSingle(); + if (accuracy != null && Math.abs(accuracy.timestamp - timestamp) < precision) { + return accuracy; + } + return null; + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + return new Select() + .from(Accuracy.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp desc, _id asc") + .limit(number) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + patched = fixUpTable(schema, patched); + return new ArrayList<>(); + } + } + + public static String evaluateAccuracy(long period) { + // TODO CACHE ? + final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl"); + final Map totals = new HashMap<>(); + final Map signed_totals = new HashMap<>(); + final Map count = new HashMap<>(); + final List alist = latestForGraph(500, JoH.tsl() - period, JoH.tsl()); + + // total up differences + for (Accuracy entry : alist) { + if (totals.containsKey(entry.plugin)) { + totals.put(entry.plugin, totals.get(entry.plugin) + Math.abs(entry.difference)); + signed_totals.put(entry.plugin, signed_totals.get(entry.plugin) + entry.difference); + count.put(entry.plugin, count.get(entry.plugin) + 1); + } else { + totals.put(entry.plugin, Math.abs(entry.difference)); + signed_totals.put(entry.plugin, entry.difference); + count.put(entry.plugin, 1); + } + } + String result = ""; + int plugin_count = 0; + for (Map.Entry total : totals.entrySet()) { + plugin_count++; + final String plugin = total.getKey(); + final int this_count = count.get(plugin); + final double this_total = total.getValue(); + // calculate the abs mean, 0 = perfect + final double this_mean = this_total / this_count; + final double signed_total = signed_totals.get(plugin); + final double signed_mean = signed_total / this_count; + // calculate the bias ratio. 0% means totally unbiased, 100% means all data skewed towards signed mean + final double signed_ratio = (Math.abs(signed_mean) / this_mean) * 100; + + if (d) UserError.Log.d(TAG, plugin + ": total: " + JoH.qs(this_total) + " count: " + this_count + " avg: " + JoH.qs(this_mean) + " mmol: " + JoH.qs((this_mean) * Constants.MGDL_TO_MMOLL) + " bias: " + JoH.qs(signed_mean) + " " + JoH.qs(signed_ratio, 0) + "%"); + String plugin_result = plugin.substring(0, 1).toLowerCase() + ": " + asString(this_mean, signed_mean, signed_ratio, domgdl); + UserError.Log.d(TAG, plugin_result); + if (result.length() > 0) result += " "; + result += plugin_result; + } + + return plugin_count == 1 ? result : result.replaceFirst(" mmol", "").replaceFirst(" mgdl", " "); + } + + private static String asString(double mean, double signed_mean, double signed_ratio, boolean domgdl) { + + String symbol = "err"; + if (signed_ratio < 90) { + symbol = "\u00B1"; // +- symbol + } else { + if (signed_mean < 0) { + symbol = "\u207B"; // superscript minus + } else { + symbol = "\u207A"; // superscript plus + } + } + return symbol + (!domgdl ? JoH.qs(mean * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(mean, 1) + " mgdl"); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBgAlert.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBgAlert.java new file mode 100644 index 0000000..f7a20e5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBgAlert.java @@ -0,0 +1,206 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.Models.UserError.Log; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; + +import java.text.DateFormat; +import java.util.Date; + +/** + * Created by Emma Black on 1/14/15. + */ +@Table(name = "ActiveBgAlert", id = BaseColumns._ID) +public class ActiveBgAlert extends Model { + + private final static String TAG = AlertPlayer.class.getSimpleName(); + private static boolean patched = false; + + @Column(name = "alert_uuid") + public String alert_uuid; + + @Column(name = "is_snoozed") + public volatile boolean is_snoozed; + + @Column(name = "last_alerted_at") // Do we need this + public volatile Long last_alerted_at; + + @Column(name = "next_alert_at") + public volatile Long next_alert_at; + + // This is needed in order to have ascending alerts + // we set the real value of it when is_snoozed is being turned to false + @Column(name = "alert_started_at") + public volatile Long alert_started_at; + + + public boolean ready_to_alarm() { + if(new Date().getTime() > next_alert_at) { + return true; + } + return false; + } + + public static boolean currentlyAlerting() { + final ActiveBgAlert activeBgAlert = getOnly(); + return activeBgAlert != null && !activeBgAlert.is_snoozed; + } + + public static boolean alertSnoozeOver() { + ActiveBgAlert activeBgAlert = getOnly(); + if (activeBgAlert == null) { + // no alert exists, so snoozing is over... (this should not happen) + Log.wtf(TAG, "ActiveBgAlert getOnly returning null (we have just checked it)"); + return true; + } + return activeBgAlert.ready_to_alarm(); + } + + public void snooze(int minutes) { + next_alert_at = new Date().getTime() + minutes * 60000; + is_snoozed = true; + Log.ueh("Snoozed Alert","Snoozed until: "+JoH.dateTimeText(next_alert_at)); + save(); + } + + public String toString() { + + try { + String alert_uuid = "alert_uuid: " + this.alert_uuid; + String is_snoozed = "is_snoozed: " + this.is_snoozed; + String last_alerted_at = "last_alerted_at: " + DateFormat.getDateTimeInstance( + DateFormat.LONG, DateFormat.LONG).format(new Date(this.last_alerted_at)); + String next_alert_at = "next_alert_at: " + DateFormat.getDateTimeInstance( + DateFormat.LONG, DateFormat.LONG).format(new Date(this.next_alert_at)); + + String alert_started_at = "alert_started_at: " + DateFormat.getDateTimeInstance( + DateFormat.LONG, DateFormat.LONG).format(new Date(this.alert_started_at)); + + return alert_uuid + " " + is_snoozed + " " + last_alerted_at + " " + next_alert_at + " " + alert_started_at; + + } catch (NullPointerException e) { + Log.e(TAG, "Got Nullpointer exception in toString()! " + e); + return "Nullpointer exception in toString!"; + } + } + + // We should only have at most one active alert at any given time. + // This means that we will only have one of this objects at the database at any given time. + // so we have the following static functions: getOnly, saveData, ClearData + + public static ActiveBgAlert getOnly() { + ActiveBgAlert aba = new Select() + .from(ActiveBgAlert.class) + .orderBy("_ID asc") + .executeSingle(); + + if (aba != null) { + Log.v(TAG, "ActiveBgAlert getOnly aba = " + aba.toString()); + } else { + Log.v(TAG, "ActiveBgAlert getOnly returning null"); + } + + return aba; + } + + public static AlertType alertTypegetOnly() { + return alertTypegetOnly(getOnly()); + } + + public static AlertType alertTypegetOnly(final ActiveBgAlert aba) { + + if (aba == null) { + Log.v(TAG, "ActiveBgAlert: alertTypegetOnly returning null"); + return null; + } + + AlertType alert = AlertType.get_alert(aba.alert_uuid); + if(alert == null) { + Log.d(TAG, "alertTypegetOnly did not find the active alert as part of existing alerts. returning null"); + // removing the alert to be in a better state. + ClearData(); + return null; + } + if(!alert.uuid.equals(aba.alert_uuid)) { + Log.wtf(TAG, "AlertType.get_alert did not return the correct alert"); + } + return alert; + } + + public static void Create(String alert_uuid, boolean is_snoozed, Long next_alert_at) { + Log.d(TAG, "ActiveBgAlert Create called"); + fixUpTable(); + ActiveBgAlert aba = getOnly(); + if (aba == null) { + aba = new ActiveBgAlert(); + } + aba.alert_uuid = alert_uuid; + aba.is_snoozed = is_snoozed; + aba.last_alerted_at = 0L; + aba.next_alert_at = next_alert_at; + aba.alert_started_at = new Date().getTime(); + aba.save(); + } + + public static void ClearData() { + Log.d(TAG, "ActiveBgAlert ClearData called"); + ActiveBgAlert aba = getOnly(); + if (aba != null) { + aba.delete(); + } + } + + public static void ClearIfSnoozeFinished() { + Log.d(TAG, "ActiveBgAlert ClearIfSnoozeFinished called"); + ActiveBgAlert aba = getOnly(); + if (aba != null) { + if(new Date().getTime() > aba.next_alert_at) { + Log.d(TAG, "ActiveBgAlert ClearIfSnoozeFinished deleting allert"); + aba.delete(); + } + } + } + + // This function is called from ClockTick, when we play + // If we were snoozed, we update the snooze to false, and update the start time. + // return the time in minutes from the time playing the alert has started + public int getAndUpdateAlertingMinutes() { + if(is_snoozed) { + is_snoozed = false; + alert_started_at = new Date().getTime(); + save(); + } + Long timeSeconds = (new Date().getTime() - alert_started_at) / 1000; + return (int)Math.round(timeSeconds / 60.0); + } + + public void updateNextAlertAt(long nextAlertTime){ + next_alert_at = nextAlertTime; + save(); + } + + private static void fixUpTable() { + if (patched) return; + String[] patchup = { + "ALTER TABLE ActiveBgAlert ADD COLUMN alert_started_at INTEGER;" + }; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBluetoothDevice.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBluetoothDevice.java new file mode 100644 index 0000000..aa2e8fc --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ActiveBluetoothDevice.java @@ -0,0 +1,62 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; + +/** + * Created by Emma Black on 11/3/14. + */ +@Table(name = "ActiveBluetoothDevice", id = BaseColumns._ID) +public class ActiveBluetoothDevice extends Model { + @Column(name = "name") + public String name; + + @Column(name = "address") + public String address; + + @Column(name = "connected") + public boolean connected; + + + public static final Object table_lock = new Object(); + + public static synchronized ActiveBluetoothDevice first() { + return new Select() + .from(ActiveBluetoothDevice.class) + .orderBy("_ID asc") + .executeSingle(); + } + + public static synchronized void forget() { + ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + if (activeBluetoothDevice != null) { + activeBluetoothDevice.delete(); + } + } + + public static synchronized void connected() { + ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + if(activeBluetoothDevice != null) { + activeBluetoothDevice.connected = true; + activeBluetoothDevice.save(); + } + } + + public static synchronized void disconnected() { + ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + if(activeBluetoothDevice != null) { + activeBluetoothDevice.connected = false; + activeBluetoothDevice.save(); + } + } + + public static synchronized boolean is_connected() { + ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + return (activeBluetoothDevice != null && activeBluetoothDevice.connected); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/AlertType.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/AlertType.java new file mode 100644 index 0000000..14203b0 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/AlertType.java @@ -0,0 +1,677 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.app.AlarmManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; + +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.Services.ActivityRecognizedService; +import com.eveningoutpost.dexdrip.Services.MissedReadingService; +import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; +import com.eveningoutpost.dexdrip.UtilityModels.Notifications; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.google.gson.internal.bind.DateTypeAdapter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * Created by Emma Black on 1/14/15. + */ +@Table(name = "AlertType", id = BaseColumns._ID) +public class AlertType extends Model { + + @Expose + @Column(name = "name") + public String name; + + @Expose + @Column(name = "active") + public boolean active; + + @Expose + @Column(name = "volume") + public int volume; + + @Expose + @Column(name = "vibrate") + public boolean vibrate; + + @Expose + @Column(name = "light") + public boolean light; + + @Expose + @Column(name = "override_silent_mode") + public boolean override_silent_mode; + + @Expose + @Column(name = "force_speaker") + public boolean force_speaker; + + @Expose + @Column(name = "predictive") + public boolean predictive; + + @Expose + @Column(name = "time_until_threshold_crossed") + public double time_until_threshold_crossed; + + // If it is not above, then it must be below. + @Expose + @Column(name = "above") + public boolean above; + + @Expose + @Column(name = "threshold") + public double threshold; + + @Expose + @Column(name = "all_day") + public boolean all_day; + + @Expose + @Column(name = "start_time_minutes") + public int start_time_minutes; // This have probable be in minutes from start of day. this is not time... + + @Expose + @Column(name = "end_time_minutes") + public int end_time_minutes; + + @Expose + @Column(name = "minutes_between") //??? what is the difference between minutes_between and default_snooze ??? + public int minutes_between; // The idea here was if ignored it will go off again each x minutes, snooze would be if it was aknowledged and dismissed it will go off again in y minutes + // that said, Im okay with doing away with the minutes between and just doing it at a set 5 mins like dex + + @Expose + @Column(name = "default_snooze") + public int default_snooze; + + @Expose + @Column(name = "text") // ??? what's that? is it different from name? + public String text; // I figured if we wanted some special text, Its + + @Expose + @Column(name = "mp3_file") + public String mp3_file; + + @Expose + @Column(name = "uuid", index = true) + public String uuid; + + public final static String LOW_ALERT_55 = "c5f1999c-4ec5-449e-adad-3980b172b920"; + private final static String TAG = Notifications.class.getSimpleName(); + private final static String TAG_ALERT = "AlertBg"; + private static boolean patched = false; + + // This shouldn't be needed but it seems it is + public static void fixUpTable() { + if (patched) return; + String[] patchup = { + "ALTER TABLE AlertType ADD COLUMN volume INTEGER;", + "ALTER TABLE AlertType ADD COLUMN light INTEGER;", + "ALTER TABLE AlertType ADD COLUMN predictive INTEGER;", + "ALTER TABLE AlertType ADD COLUMN text TEXT;", + "ALTER TABLE AlertType ADD COLUMN force_speaker INTEGER;", + "ALTER TABLE AlertType ADD COLUMN time_until_threshold_crossed REAL;" + }; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } + + + + public static AlertType get_alert(String uuid) { + + return new Select() + .from(AlertType.class) + .where("uuid = ? ", uuid) + .executeSingle(); + } + + /* + * This function has 3 needs. In the case of "unclear state" return null + * In the case of "unclear state" for more than predefined time, return the "55" alert + * In case that alerts are turned off, only return the 55. + */ + public static AlertType get_highest_active_alert(Context context, double bg) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ + Log.d("NOTIFICATIONS", "Notifications are currently disabled!!"); + return null; + } + + if (bg <= 14) { // Special dexcom codes should not set off low alarms + return null; + } + + AlertType at; + at = get_highest_active_alert_helper(bg, prefs); + if (at != null) { + Log.d(TAG_ALERT, "get_highest_active_alert_helper returned alert uuid = " + at.uuid + " alert name = " + at.name); + } else { + Log.d(TAG_ALERT, "get_highest_active_alert_helper returned NULL"); + } + return at; + } + + private static AlertType filter_alert_on_stale(AlertType alert, SharedPreferences prefs) + { + // this should already be happening in notifications.java but it doesn't seem to work so adding here as well + if (prefs.getBoolean("disable_alerts_stale_data", false)) { + final int stale_minutes = Math.max(6, Integer.parseInt(prefs.getString("disable_alerts_stale_data_minutes", "15")) + 2); + if (!BgReading.last_within_minutes(stale_minutes)) { + Log.w(TAG, "Blocking alarm raise as data older than: " + stale_minutes); + return null; // block + } + } + return alert; // allow + } + + // bg_minute is the estimatin of the bg change rate + private static AlertType get_highest_active_alert_helper(double bg, SharedPreferences prefs) { + // Chcek the low alerts + + final double offset = ActivityRecognizedService.raise_limit_due_to_vehicle_mode() ? ActivityRecognizedService.getVehicle_mode_adjust_mgdl() : 0; + + if(prefs.getLong("low_alerts_disabled_until", 0) > new Date().getTime()){ + Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: Low alerts are currently disabled!! Skipping low alerts"); + + } else { + List lowAlerts = new Select() + .from(AlertType.class) + .where("threshold >= ?", bg-offset) + .where("above = ?", false) + .orderBy("threshold asc") + .execute(); + + for (AlertType lowAlert : lowAlerts) { + if(lowAlert.should_alarm(bg-offset)) { + return filter_alert_on_stale(lowAlert,prefs); + } + } + } + + + // If no low alert found or low alerts disabled, check higher alert. + if(prefs.getLong("high_alerts_disabled_until", 0) > new Date().getTime()){ + Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: High alerts are currently disabled!! Skipping high alerts"); + ; + } else { + List HighAlerts = new Select() + .from(AlertType.class) + .where("threshold <= ?", bg) + .where("above = ?", true) + .orderBy("threshold desc") + .execute(); + + for (AlertType HighAlert : HighAlerts) { + //Log.e(TAG, "Testing high alert " + HighAlert.toString()); + if(HighAlert.should_alarm(bg)) { + return filter_alert_on_stale(HighAlert,prefs); + } + } + } + // no alert found + return null; + } + + // returns true, if one allert is up and the second is down + public static boolean OpositeDirection(AlertType a1, AlertType a2) { + if (a1.above != a2.above) { + return true; + } + return false; + } + + // Checks if a1 is more important than a2. returns the higher one + public static AlertType HigherAlert(AlertType a1, AlertType a2) { + if (a1.above && !a2.above) { + return a2; + } + if (!a1.above && a2.above) { + return a1; + } + if (a1.above && a2.above) { + // both are high, the higher the better + if (a1.threshold > a2.threshold) { + return a1; + } else { + return a2; + } + } + if (a1.above || a2.above) { + Log.wtf(TAG, "a1.above and a2.above must be false"); + } + // both are low, the lower the better + if (a1.threshold < a2.threshold) { + return a1; + } else { + return a2; + } + } + + public static void remove_all() { + List Alerts = new Select() + .from(AlertType.class) + .execute(); + + for (AlertType alert : Alerts) { + alert.delete(); + } + ActiveBgAlert.ClearData(); + } + + public static void add_alert( + String uuid, + String name, + boolean above, + double threshold, + boolean all_day, + int minutes_between, + String mp3_file, + int start_time_minutes, + int end_time_minutes, + boolean override_silent_mode, + boolean force_speaker, + int snooze, + boolean vibrate, + boolean active) { + AlertType at = new AlertType(); + at.name = name; + at.above = above; + at.threshold = threshold; + at.all_day = all_day; + at.minutes_between = minutes_between; + at.uuid = uuid != null? uuid : UUID.randomUUID().toString(); + at.active = active; + at.mp3_file = mp3_file; + at.start_time_minutes = start_time_minutes; + at.end_time_minutes = end_time_minutes; + at.override_silent_mode = override_silent_mode; + at.force_speaker = force_speaker; + at.default_snooze = snooze; + at.vibrate = vibrate; + at.save(); + } + + public static void update_alert( + String uuid, + String name, + boolean above, + double threshold, + boolean all_day, + int minutes_between, + String mp3_file, + int start_time_minutes, + int end_time_minutes, + boolean override_silent_mode, + boolean force_speaker, + int snooze, + boolean vibrate, + boolean active) { + + fixUpTable(); + + final AlertType at = get_alert(uuid); + if (at == null) { + Log.e(TAG, "Alert Type null during update"); + return; + } + at.name = name; + at.above = above; + at.threshold = threshold; + at.all_day = all_day; + at.minutes_between = minutes_between; + at.uuid = uuid; + at.active = active; + at.mp3_file = mp3_file; + at.start_time_minutes = start_time_minutes; + at.end_time_minutes = end_time_minutes; + at.override_silent_mode = override_silent_mode; + at.force_speaker = force_speaker; + at.default_snooze = snooze; + at.vibrate = vibrate; + at.save(); + } + public static void remove_alert(String uuid) { + AlertType alert = get_alert(uuid); + if(alert != null) { + alert.delete(); + } + } + + public String toString() { + + String name = "name: " + this.name; + String above = "above: " + this.above; + String threshold = "threshold: " + this.threshold; + String all_day = "all_day: " + this.all_day; + String time = "Start time: " + this.start_time_minutes + " end time: "+ this.end_time_minutes; + String minutes_between = "minutes_between: " + this.minutes_between; + String uuid = "uuid: " + this.uuid; + + return name + " " + above + " " + threshold + " "+ all_day + " " +time +" " + minutes_between + " uuid" + uuid; + } + + public String toS() { + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + return gson.toJson(this); + } + + public static void print_all() { + List Alerts = new Select() + .from(AlertType.class) + .execute(); + + Log.d(TAG,"List of all alerts"); + for (AlertType alert : Alerts) { + Log.d(TAG, alert.toString()); + } + } + + // get the first item in the alert list which is active for either high or low alert, sorted by when the threshold will be hit, eg the highest low alert or the lowest high alert + public static double getFirstActiveAlertThreshold(final boolean highAlert) { + final List list = getAll(highAlert); + if (list != null) { + for (final AlertType alert : list) { + if (alert.active) return alert.threshold; + } + } + return -1; + } + + public static List getAllActive() { + List alerts = new Select() + .from(AlertType.class) + .where("active = ?", true) + .execute(); + + return alerts; + } + + public static List getAll(boolean above) { + String order; + if (above) { + order = "threshold asc"; + } else { + order = "threshold desc"; + } + List alerts = new Select() + .from(AlertType.class) + .where("above = ?", above) + .orderBy(order) + .execute(); + + return alerts; + } + + public static boolean activeLowAlertExists() { + List alerts = getAll(false); + if(alerts == null) { + return false; + } + for (AlertType alert : alerts) { + if(alert.active) { + return true; + } + } + return false; + } + + // This function is used to make sure that we always have a static alert on 55 low. + // This alert will not be editable/removable. + public static void CreateStaticAlerts() { + if(get_alert(LOW_ALERT_55) == null) { + add_alert(LOW_ALERT_55, "low alert ", false, 55, true, 1, null, 0, 0, true, true, 20, true, true); + } + } + + + public static void testAll(Context context) { + remove_all(); + add_alert(null, "high alert 1", true, 180, true, 10, null, 0, 0, true, true, 20, true, true); + add_alert(null, "high alert 2", true, 200, true, 10, null, 0, 0, true, true,20, true, true); + add_alert(null, "high alert 3", true, 220, true, 10, null, 0, 0, true, true,20, true, true); + print_all(); + AlertType a1 = get_highest_active_alert(context, 190); + Log.d(TAG, "a1 = " + a1.toString()); + AlertType a2 = get_highest_active_alert(context, 210); + Log.d(TAG, "a2 = " + a2.toString()); + + + AlertType a3 = get_alert(a1.uuid); + Log.d(TAG, "a1 == a3 ? need to see true " + (a1==a3) + a1 + " " + a3); + + add_alert(null, "low alert 1", false, 80, true, 10, null, 0, 0, true, true,20, true, true); + add_alert(null, "low alert 2", false, 60, true, 10, null, 0, 0, true, true,20, true, true); + + AlertType al1 = get_highest_active_alert(context, 90); + Log.d(TAG, "al1 should be null " + al1); + al1 = get_highest_active_alert(context, 80); + Log.d(TAG, "al1 = " + al1.toString()); + AlertType al2 = get_highest_active_alert(context, 50); + Log.d(TAG, "al2 = " + al2.toString()); + + Log.d(TAG, "HigherAlert(a1, a2) = a1?" + (HigherAlert(a1,a2) == a2)); + Log.d(TAG, "HigherAlert(al1, al2) = al1?" + (HigherAlert(al1,al2) == al2)); + Log.d(TAG, "HigherAlert(a1, al1) = al1?" + (HigherAlert(a1,al1) == al1)); + Log.d(TAG, "HigherAlert(al1, a2) = al1?" + (HigherAlert(al1,a2) == al1)); + + // Make sure we do not influance on real data... + remove_all(); + + } + + + private boolean in_time_frame() { + return s_in_time_frame(all_day, start_time_minutes, end_time_minutes); + } + + static public boolean s_in_time_frame(boolean s_all_day, int s_start_time_minutes, int s_end_time_minutes) { + if (s_all_day) { + //Log.e(TAG, "in_time_frame returning true " ); + return true; + } + // time_now is the number of minutes that have passed from the start of the day. + Calendar rightNow = Calendar.getInstance(); + int time_now = toTime(rightNow.get(Calendar.HOUR_OF_DAY), rightNow.get(Calendar.MINUTE)); + Log.d(TAG, "time_now is " + time_now + " minutes" + " start_time " + s_start_time_minutes + " end_time " + s_end_time_minutes); + if(s_start_time_minutes < s_end_time_minutes) { + if (time_now >= s_start_time_minutes && time_now <= s_end_time_minutes) { + return true; + } + } else { + if (time_now >= s_start_time_minutes || time_now <= s_end_time_minutes) { + return true; + } + } + return false; + } + + private boolean beyond_threshold(double bg) { + if (above && bg >= threshold) { +// Log.e(TAG, "beyond_threshold returning true " ); + return true; + } else if (!above && bg <= threshold) { + return true; + } + return false; + } + + private boolean trending_to_threshold(double bg) { + if (!predictive) { return false; } + if (above && bg >= threshold) { + return true; + } else if (!above && bg <= threshold) { + return true; + } + return false; + } + + public long getNextAlertTime(Context ctx) { + int time = minutes_between; + if (time < 1 || AlertPlayer.isAscendingMode(ctx)) { + time = 1; + } + Calendar calendar = Calendar.getInstance(); + return calendar.getTimeInMillis() + (time * 60000); + } + + public boolean should_alarm(double bg) { +// Log.e(TAG, "should_alarm called active = " + active ); + if(in_time_frame() && active && (beyond_threshold(bg) || trending_to_threshold(bg))) { + return true; + } else { + return false; + } + } + + public static void testAlert( + String name, + boolean above, + double threshold, + boolean all_day, + int minutes_between, + String mp3_file, + int start_time_minutes, + int end_time_minutes, + boolean override_silent_mode, + boolean force_speaker, + int snooze, + boolean vibrate, + Context context) { + AlertType at = new AlertType(); + at.name = name; + at.above = above; + at.threshold = threshold; + at.all_day = all_day; + at.minutes_between = minutes_between; + at.uuid = UUID.randomUUID().toString(); + at.active = true; + at.mp3_file = mp3_file; + at.start_time_minutes = start_time_minutes; + at.end_time_minutes = end_time_minutes; + at.override_silent_mode = override_silent_mode; + at.force_speaker = force_speaker; + at.default_snooze = snooze; + at.vibrate = vibrate; + AlertPlayer.getPlayer().startAlert(context, false, at, "TEST", false); + } + + // Time is calculated in minutes. that is 01:20 means 80 minutes. + + // This functions are a bit tricky. We can only set time from 00:00 to 23:59 which leaves one minute out. this is because we ignore the + // seconds. so if the user has set 23:59 we will consider this as 24:00 + // This will be done at the code that reads the time from the ui. + + + + // return the minutes part of the time + public static int time2Minutes(int minutes) { + return (minutes - 60*time2Hours(minutes)) ; + } + + // return the hours part of the time + public static int time2Hours(int minutes) { + return minutes / 60; + } + + // create the time from hours and minutes. + public static int toTime(int hours, int minutes) { + return hours * 60 + minutes; + } + + // Convert all settings to a string and save it in the references. This is needed to allow it's backup. + public static boolean toSettings(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + List alerts = new Select() + .from(AlertType.class) + .execute(); + + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + String output = gson.toJson(alerts); + Log.e(TAG, "Created the string " + output); + prefs.edit().putString("saved_alerts", output).commit(); // always leave this as commit + + return true; + + } + + + // Read all alerts from preference key and write them to db. + public static boolean fromSettings(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String savedAlerts = prefs.getString("saved_alerts", ""); + if (savedAlerts.isEmpty()) { + Log.i(TAG, "read saved_alerts string and it is empty"); + return true; + } + Log.i(TAG, "read alerts string " + savedAlerts); + + AlertType[] newAlerts = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(savedAlerts, AlertType[].class); + if (newAlerts == null) { + Log.e(TAG, "newAlerts is null"); + return true; + } + + Log.i(TAG, "read successfuly " + newAlerts.length); + // Now delete all existing alerts if we managed to unpack the json + try { + List alerts = new Select() + .from(AlertType.class) + .execute(); + for (AlertType alert : alerts) { + alert.delete(); + } + } catch (NullPointerException e) { + Log.e(TAG, "Got null pointer exception: " + e); + } + + try { + for (AlertType alert : newAlerts) { + Log.e(TAG, "Saving alert " + alert.name); + alert.save(); + } + } catch (NullPointerException e) { + Log.e(TAG, "Got null pointer exception 2: " + e); + } + // Delete the string, so next time we will not load the data + prefs.edit().putString("saved_alerts", "").apply(); + return true; + + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/BgReading.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/BgReading.java new file mode 100644 index 0000000..7c328b9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/BgReading.java @@ -0,0 +1,2334 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.BestGlucose; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService; +import com.eveningoutpost.dexdrip.Services.SyncService; +import com.eveningoutpost.dexdrip.ShareModels.ShareUploadableBg; +import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder; +import com.eveningoutpost.dexdrip.UtilityModels.BgSendQueue; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; +import com.eveningoutpost.dexdrip.UtilityModels.Notifications; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue; +import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse; +import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract; +import com.eveningoutpost.dexdrip.messages.BgReadingMessage; +import com.eveningoutpost.dexdrip.messages.BgReadingMultiMessage; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.eveningoutpost.dexdrip.utils.SqliteRejigger; +import com.eveningoutpost.dexdrip.wearintegration.WatchUpdaterService; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.google.gson.internal.bind.DateTypeAdapter; +import com.squareup.wire.Wire; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.getCalibrationPluginFromPreferences; +import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.newCloseSensorData; + +@Table(name = "BgReadings", id = BaseColumns._ID) +public class BgReading extends Model implements ShareUploadableBg { + + private final static String TAG = BgReading.class.getSimpleName(); + private final static String TAG_ALERT = TAG + " AlertBg"; + private final static String PERSISTENT_HIGH_SINCE = "persistent_high_since"; + public static final double AGE_ADJUSTMENT_TIME = 86400000 * 1.9; + public static final double AGE_ADJUSTMENT_FACTOR = .45; + //TODO: Have these as adjustable settings!! + public final static double BESTOFFSET = (60000 * 0); // Assume readings are about x minutes off from actual! + + public static final int BG_READING_ERROR_VALUE = 38; // error marker + public static final int BG_READING_MINIMUM_VALUE = 39; + public static final int BG_READING_MAXIMUM_VALUE = 400; + + private static volatile long earliest_backfill = 0; + + @Column(name = "sensor", index = true) + public Sensor sensor; + + @Column(name = "calibration", index = true, onDelete = Column.ForeignKeyAction.CASCADE) + public Calibration calibration; + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + @Expose + @Column(name = "time_since_sensor_started") + public double time_since_sensor_started; + + @Expose + @Column(name = "raw_data") + public volatile double raw_data; + + @Expose + @Column(name = "filtered_data") + public double filtered_data; + + @Expose + @Column(name = "age_adjusted_raw_value") + public double age_adjusted_raw_value; + + @Expose + @Column(name = "calibration_flag") + public boolean calibration_flag; + + @Expose + @Column(name = "calculated_value") + public double calculated_value; + + @Expose + @Column(name = "filtered_calculated_value") + public double filtered_calculated_value; + + @Expose + @Column(name = "calculated_value_slope") + public double calculated_value_slope; + + @Expose + @Column(name = "a") + public double a; + + @Expose + @Column(name = "b") + public double b; + + @Expose + @Column(name = "c") + public double c; + + @Expose + @Column(name = "ra") + public double ra; + + @Expose + @Column(name = "rb") + public double rb; + + @Expose + @Column(name = "rc") + public double rc; + @Expose + // TODO unification with wear support ConflictAction.REPLACE for wear, done with rejig below + @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public String uuid; + + @Expose + @Column(name = "calibration_uuid") + public String calibration_uuid; + + @Expose + @Column(name = "sensor_uuid", index = true) + public String sensor_uuid; + + // mapped to the no longer used "synced" to keep DB Scheme compatible + @Expose + @Column(name = "snyced") + public boolean ignoreForStats; + + @Expose + @Column(name = "raw_calculated") + public double raw_calculated; + + @Expose + @Column(name = "hide_slope") + public boolean hide_slope; + + @Expose + @Column(name = "noise") + public String noise; + + @Expose + @Column(name = "dg_mgdl") + public double dg_mgdl = 0d; + + @Expose + @Column(name = "dg_slope") + public double dg_slope = 0d; + + @Expose + @Column(name = "dg_delta_name") + public String dg_delta_name; + + @Expose + @Column(name = "source_info") + public volatile String source_info; + + public synchronized static void updateDB() { + final String[] updates = new String[]{"ALTER TABLE BgReadings ADD COLUMN dg_mgdl REAL;", + "ALTER TABLE BgReadings ADD COLUMN dg_slope REAL;", + "ALTER TABLE BgReadings ADD COLUMN dg_delta_name TEXT;", + "ALTER TABLE BgReadings ADD COLUMN source_info TEXT;"}; + for (String patch : updates) { + try { + SQLiteUtils.execSql(patch); + } catch (Exception e) { + } + } + + // needs different handling on wear + if (JoH.areWeRunningOnAndroidWear()) { + BgSendQueue.emptyQueue(); + SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT FAIL", "uuid TEXT UNIQUE ON CONFLICT REPLACE"); + SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT IGNORE", "uuid TEXT UNIQUE ON CONFLICT REPLACE"); + SqliteRejigger.rejigSchema("BgSendQueue", "BgReadings_temp", "BgReadings"); + } + + } + + public double getDg_mgdl(){ + if(dg_mgdl != 0) return dg_mgdl; + return calculated_value; + } + + public double getDg_slope(){ + if(dg_mgdl != 0) return dg_slope; + if(calculated_value_slope !=0) return calculated_value_slope; + return currentSlope(); + } + + public String getDg_deltaName(){ + if(dg_mgdl != 0 && dg_delta_name != null) return dg_delta_name; + return slopeName(); + } + + public double calculated_value_mmol() { + return mmolConvert(calculated_value); + } + + public void injectDisplayGlucose(BestGlucose.DisplayGlucose displayGlucose) { + //displayGlucose can be null. E.g. when out of order values come in + if (displayGlucose != null) { + if (Math.abs(displayGlucose.timestamp - timestamp) < Constants.MINUTE_IN_MS * 10) { + dg_mgdl = displayGlucose.mgdl; + dg_slope = displayGlucose.slope; + dg_delta_name = displayGlucose.delta_name; + // TODO we probably should reflect the display glucose delta here as well for completeness + this.save(); + } else { + if (JoH.ratelimit("cannotinjectdg", 30)) { + UserError.Log.e(TAG, "Cannot inject display glucose value as time difference too great: " + JoH.dateTimeText(displayGlucose.timestamp) + " vs " + JoH.dateTimeText(timestamp)); + } + } + } + } + + public double mmolConvert(double mgdl) { + return mgdl * Constants.MGDL_TO_MMOLL; + } + + public String displayValue(Context context) { + final String unit = Pref.getString("units", "mgdl"); + final DecimalFormat df = new DecimalFormat("#"); + final double this_value = getDg_mgdl(); + if (this_value >= 400) { + return "HIGH"; + } else if (this_value >= 40) { + if (unit.equals("mgdl")) { + df.setMaximumFractionDigits(0); + return df.format(this_value); + } else { + df.setMaximumFractionDigits(1); + return df.format(mmolConvert(this_value)); + } + } else { + return "LOW"; + // TODO doesn't understand special low values + } + } + + public static double activeSlope() { + BgReading bgReading = BgReading.lastNoSenssor(); + if (bgReading != null) { + double slope = (2 * bgReading.a * (new Date().getTime() + BESTOFFSET)) + bgReading.b; + Log.i(TAG, "ESTIMATE SLOPE" + slope); + return slope; + } + return 0; + } + + public static double activePrediction() { + BgReading bgReading = BgReading.lastNoSenssor(); + if (bgReading != null) { + double currentTime = new Date().getTime(); + if (currentTime >= bgReading.timestamp + (60000 * 7)) { + currentTime = bgReading.timestamp + (60000 * 7); + } + double time = currentTime + BESTOFFSET; + return ((bgReading.a * time * time) + (bgReading.b * time) + bgReading.c); + } + return 0; + } + + + public static double calculateSlope(BgReading current, BgReading last) { + if (current.timestamp == last.timestamp || current.calculated_value == last.calculated_value) { + return 0; + } else { + return (last.calculated_value - current.calculated_value) / (last.timestamp - current.timestamp); + } + } + + public static double currentSlope() { + return currentSlope(Home.get_follower()); + } + + public static double currentSlope(boolean is_follower) { + List last_2 = BgReading.latest(2, is_follower); + if ((last_2 != null) && (last_2.size() == 2)) { + double slope = calculateSlope(last_2.get(0), last_2.get(1)); + return slope; + } else { + return 0d; + } + } + + + //*******CLASS METHODS***********// + // Dexcom Bluetooth Share + public static void create(EGVRecord[] egvRecords, long addativeOffset, Context context) { + for (EGVRecord egvRecord : egvRecords) { + BgReading.create(egvRecord, addativeOffset, context); + } + } + + public static void create(SensorRecord[] sensorRecords, long addativeOffset, Context context) { + for (SensorRecord sensorRecord : sensorRecords) { + BgReading.create(sensorRecord, addativeOffset, context); + } + } + + public static void create(SensorRecord sensorRecord, long addativeOffset, Context context) { + Log.i(TAG, "create: gonna make some sensor records: " + sensorRecord.getUnfiltered()); + if (BgReading.is_new(sensorRecord, addativeOffset)) { + BgReading bgReading = new BgReading(); + Sensor sensor = Sensor.currentSensor(); + Calibration calibration = Calibration.getForTimestamp(sensorRecord.getSystemTime().getTime() + addativeOffset); + if (sensor != null && calibration != null) { + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.calibration = calibration; + bgReading.calibration_uuid = calibration.uuid; + bgReading.raw_data = (sensorRecord.getUnfiltered() / 1000); + bgReading.filtered_data = (sensorRecord.getFiltered() / 1000); + bgReading.timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset; + if (bgReading.timestamp > new Date().getTime()) { + return; + } + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; + bgReading.calculateAgeAdjustedRawValue(); + bgReading.save(); + } + } + } + + // Dexcom Bluetooth Share + public static void create(EGVRecord egvRecord, long addativeOffset, Context context) { + BgReading bgReading = BgReading.getForTimestamp(egvRecord.getSystemTime().getTime() + addativeOffset); + Log.i(TAG, "create: Looking for BG reading to tag this thing to: " + egvRecord.getBGValue()); + if (bgReading != null) { + bgReading.calculated_value = egvRecord.getBGValue(); + if (egvRecord.getBGValue() <= 13) { + Calibration calibration = bgReading.calibration; + double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10))); + double calSlope = (calibration.first_scale / firstAdjSlope) * 1000; + double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1; + bgReading.raw_calculated = (((calSlope * bgReading.raw_data) + calIntercept) - 5); + } + Log.i(TAG, "create: NEW VALUE CALCULATED AT: " + bgReading.calculated_value); + bgReading.calculated_value_slope = bgReading.slopefromName(egvRecord.getTrend().friendlyTrendName()); + bgReading.noise = egvRecord.noiseValue(); + String friendlyName = egvRecord.getTrend().friendlyTrendName(); + if (friendlyName.compareTo("NONE") == 0 || + friendlyName.compareTo("NOT_COMPUTABLE") == 0 || + friendlyName.compareTo("NOT COMPUTABLE") == 0 || + friendlyName.compareTo("OUT OF RANGE") == 0 || + friendlyName.compareTo("OUT_OF_RANGE") == 0) { + bgReading.hide_slope = true; + } + bgReading.save(); + bgReading.find_new_curve(); + bgReading.find_new_raw_curve(); + //context.startService(new Intent(context, Notifications.class)); + Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading + BgSendQueue.handleNewBgReading(bgReading, "create", context); + } + } + + public static BgReading getForTimestamp(double timestamp) { + Sensor sensor = Sensor.currentSensor(); + if (sensor != null) { + BgReading bgReading = new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) + .where("calculated_value = 0") + .where("raw_calculated = 0") + .orderBy("timestamp desc") + .executeSingle(); + if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? + Log.i(TAG, "getForTimestamp: Found a BG timestamp match"); + return bgReading; + } + } + Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match"); + return null; + } + + // used in wear + public static BgReading getForTimestampExists(double timestamp) { + Sensor sensor = Sensor.currentSensor(); + if (sensor != null) { + BgReading bgReading = new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) + .orderBy("timestamp desc") + .executeSingle(); + if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? + Log.i(TAG, "getForTimestamp: Found a BG timestamp match"); + return bgReading; + } + } + Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match"); + return null; + } + + public static BgReading getForPreciseTimestamp(long timestamp, long precision) { + return getForPreciseTimestamp(timestamp, precision, true); + } + + public static BgReading getForPreciseTimestamp(long timestamp, long precision, boolean lock_to_sensor) { + final Sensor sensor = Sensor.currentSensor(); + if ((sensor != null) || !lock_to_sensor) { + final BgReading bgReading = new Select() + .from(BgReading.class) + .where(lock_to_sensor ? "Sensor = ?" : "timestamp > ?", (lock_to_sensor ? sensor.getId() : 0)) + .where("timestamp <= ?", (timestamp + precision)) + .where("timestamp >= ?", (timestamp - precision)) + .orderBy("abs(timestamp - " + timestamp + ") asc") + .executeSingle(); + if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < precision) { //cool, so was it actually within precision of that bg reading? + //Log.d(TAG, "getForPreciseTimestamp: Found a BG timestamp match"); + return bgReading; + } + } + Log.d(TAG, "getForPreciseTimestamp: No luck finding a BG timestamp match: " + JoH.dateTimeText((long) timestamp) + " precision:" + precision + " Sensor: " + ((sensor == null) ? "null" : sensor.getId())); + return null; + } + + + public static boolean is_new(SensorRecord sensorRecord, long addativeOffset) { + double timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset; + Sensor sensor = Sensor.currentSensor(); + if (sensor != null) { + BgReading bgReading = new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) + .orderBy("timestamp desc") + .executeSingle(); + if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? + Log.i(TAG, "isNew; Old Reading"); + return false; + } + } + Log.i(TAG, "isNew: New Reading"); + return true; + } + + public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp) { + return create(raw_data, filtered_data, context, timestamp, false); + } + + public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp, boolean quick) { + if (context == null) context = xdrip.getAppContext(); + BgReading bgReading = new BgReading(); + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.i("BG GSON: ", bgReading.toS()); + return bgReading; + } + + if (raw_data == 0) { + Log.e(TAG,"Warning: raw_data is 0 in BgReading.create()"); + } + + Calibration calibration = Calibration.lastValid(); + if (calibration == null) { + Log.d(TAG, "create: No calibration yet"); + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.raw_data = (raw_data / 1000); + bgReading.filtered_data = (filtered_data / 1000); + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; + bgReading.calibration_flag = false; + + bgReading.calculateAgeAdjustedRawValue(); + + bgReading.save(); + bgReading.perform_calculations(); + BgSendQueue.sendToPhone(context); + } else { + Log.d(TAG, "Calibrations, so doing everything: " + calibration.uuid); + bgReading = createFromRawNoSave(sensor, calibration, raw_data, filtered_data, timestamp); + + bgReading.save(); + + // used when we are not fast inserting data + if (!quick) { + bgReading.perform_calculations(); + + if (JoH.ratelimit("opportunistic-calibration", 60)) { + BloodTest.opportunisticCalibration(); + } + + //context.startService(new Intent(context, Notifications.class)); + // allow this instead to be fired inside handleNewBgReading when noise will have been injected already + } + + bgReading.postProcess(quick); + + } + + Log.i("BG GSON: ", bgReading.toS()); + + return bgReading; + } + + public void postProcess(final boolean quick) { + injectNoise(true); // Add noise parameter for nightscout + injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout + BgSendQueue.handleNewBgReading(this, "create", xdrip.getAppContext(), Home.get_follower(), quick); + } + + public static BgReading createFromRawNoSave(Sensor sensor, Calibration calibration, double raw_data, double filtered_data, long timestamp) { + final BgReading bgReading = new BgReading(); + if (sensor == null) { + sensor = Sensor.currentSensor(); + if (sensor == null) { + return bgReading; + } + } + if (calibration == null) { + calibration = Calibration.lastValid(); + if (calibration == null) { + return bgReading; + } + } + + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.calibration = calibration; + bgReading.calibration_uuid = calibration.uuid; + bgReading.raw_data = (raw_data / 1000); + bgReading.filtered_data = (filtered_data / 1000); + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; + + bgReading.calculateAgeAdjustedRawValue(); + + if (calibration.check_in) { + double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10))); + double calSlope = (calibration.first_scale / firstAdjSlope) * 1000; + double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1; + bgReading.calculated_value = (((calSlope * bgReading.raw_data) + calIntercept) - 5); + bgReading.filtered_calculated_value = (((calSlope * bgReading.ageAdjustedFiltered()) + calIntercept) - 5); + + } else { + BgReading lastBgReading = BgReading.last(); + if (lastBgReading != null && lastBgReading.calibration != null) { + Log.d(TAG, "Create calibration.uuid=" + calibration.uuid + " bgReading.uuid: " + bgReading.uuid + " lastBgReading.calibration_uuid: " + lastBgReading.calibration_uuid + " lastBgReading.calibration.uuid: " + lastBgReading.calibration.uuid); + Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + bgReading.timestamp + " lastBgReading.timestamp: " + lastBgReading.timestamp + " lastBgReading.calibration.timestamp: " + lastBgReading.calibration.timestamp); + Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + JoH.dateTimeText(bgReading.timestamp) + " lastBgReading.timestamp: " + JoH.dateTimeText(lastBgReading.timestamp) + " lastBgReading.calibration.timestamp: " + JoH.dateTimeText(lastBgReading.calibration.timestamp)); + if (lastBgReading.calibration_flag == true && ((lastBgReading.timestamp + (60000 * 20)) > bgReading.timestamp) && ((lastBgReading.calibration.timestamp + (60000 * 20)) > bgReading.timestamp)) { + lastBgReading.calibration.rawValueOverride(BgReading.weightedAverageRaw(lastBgReading.timestamp, bgReading.timestamp, lastBgReading.calibration.timestamp, lastBgReading.age_adjusted_raw_value, bgReading.age_adjusted_raw_value), xdrip.getAppContext()); + newCloseSensorData(); + } + } + + if ((bgReading.raw_data != 0) && (bgReading.raw_data * 2 == bgReading.filtered_data)) { + Log.wtf(TAG, "Filtered data is exactly double raw - this is completely wrong - dead transmitter? - blocking glucose calculation"); + bgReading.calculated_value = 0; + bgReading.filtered_calculated_value = 0; + bgReading.hide_slope = true; + } else if (!SensorSanity.isRawValueSane(bgReading.raw_data)) { + Log.wtf(TAG, "Raw data fails sanity check! " + bgReading.raw_data); + bgReading.calculated_value = 0; + bgReading.filtered_calculated_value = 0; + bgReading.hide_slope = true; + } else { + + // calculate glucose number from raw + final CalibrationAbstract.CalibrationData pcalibration; + final CalibrationAbstract plugin = getCalibrationPluginFromPreferences(); // make sure do this only once + + if ((plugin != null) && ((pcalibration = plugin.getCalibrationData()) != null) && (Pref.getBoolean("use_pluggable_alg_as_primary", false))) { + Log.d(TAG, "USING CALIBRATION PLUGIN AS PRIMARY!!!"); + if (plugin.isCalibrationSane(pcalibration)) { + bgReading.calculated_value = (pcalibration.slope * bgReading.age_adjusted_raw_value) + pcalibration.intercept; + bgReading.filtered_calculated_value = (pcalibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept; + } else { + UserError.Log.wtf(TAG, "Calibration plugin failed intercept sanity check: " + pcalibration.toS()); + Home.toaststaticnext("Calibration plugin failed intercept sanity check"); + } + } else { + bgReading.calculated_value = ((calibration.slope * bgReading.age_adjusted_raw_value) + calibration.intercept); + bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept); + } + + updateCalculatedValueToWithinMinMax(bgReading); + } + } + + // LimiTTer can send 12 to indicate problem with NFC reading. + if ((!calibration.check_in) && (raw_data == 12) && (filtered_data == 12)) { + // store the raw value for sending special codes, note updateCalculatedValue would try to nix it + bgReading.calculated_value = raw_data; + bgReading.filtered_calculated_value = filtered_data; + } + return bgReading; + } + + public static boolean isRawMarkerValue(final double raw_data) { + return raw_data == BgReading.SPECIAL_G5_PLACEHOLDER + || raw_data == BgReading.SPECIAL_RAW_NOT_AVAILABLE; + } + + + static void updateCalculatedValueToWithinMinMax(BgReading bgReading) { + // TODO should this really be <10 other values also special?? + if (bgReading.calculated_value < 10) { + bgReading.calculated_value = BG_READING_ERROR_VALUE; + bgReading.hide_slope = true; + } else { + bgReading.calculated_value = Math.min(BG_READING_MAXIMUM_VALUE, Math.max(BG_READING_MINIMUM_VALUE, bgReading.calculated_value)); + } + Log.i(TAG, "NEW VALUE CALCULATED AT: " + bgReading.calculated_value); + } + + // Used by xDripViewer + public static void create(Context context, double raw_data, double age_adjusted_raw_value, double filtered_data, Long timestamp, + double calculated_bg, double calculated_current_slope, boolean hide_slope) { + + BgReading bgReading = new BgReading(); + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.w(TAG, "No sensor, ignoring this bg reading"); + return; + } + + Calibration calibration = Calibration.lastValid(); + if (calibration == null) { + Log.d(TAG, "create: No calibration yet"); + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.raw_data = (raw_data / 1000); + bgReading.age_adjusted_raw_value = age_adjusted_raw_value; + bgReading.filtered_data = (filtered_data / 1000); + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.calculated_value = calculated_bg; + bgReading.calculated_value_slope = calculated_current_slope; + bgReading.hide_slope = hide_slope; + + bgReading.save(); + bgReading.perform_calculations(); + } else { + Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading); + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.calibration = calibration; + bgReading.calibration_uuid = calibration.uuid; + bgReading.raw_data = (raw_data / 1000); + bgReading.age_adjusted_raw_value = age_adjusted_raw_value; + bgReading.filtered_data = (filtered_data / 1000); + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.calculated_value = calculated_bg; + bgReading.calculated_value_slope = calculated_current_slope; + bgReading.hide_slope = hide_slope; + + bgReading.save(); + } + BgSendQueue.handleNewBgReading(bgReading, "create", context); + + Log.i("BG GSON: ", bgReading.toS()); + } + + public static void pushBgReadingSyncToWatch(BgReading bgReading, boolean is_new) { + Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); + if (Pref.getBooleanDefaultFalse("wear_sync")) { + if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bgReading) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + + public String displaySlopeArrow() { + return slopeToArrowSymbol(this.dg_mgdl > 0 ? this.dg_slope * 60000 : this.calculated_value_slope * 60000); + } + + public static String activeSlopeArrow() { + double slope = (float) (BgReading.activeSlope() * 60000); + return slopeToArrowSymbol(slope); + } + + public static String slopeToArrowSymbol(double slope) { + if (slope <= (-3.5)) { + return "\u21ca";// ⇊ + } else if (slope <= (-2)) { + return "\u2193"; // ↓ + } else if (slope <= (-1)) { + return "\u2198"; // ↘ + } else if (slope <= (1)) { + return "\u2192"; // → + } else if (slope <= (2)) { + return "\u2197"; // ↗ + } else if (slope <= (3.5)) { + return "\u2191"; // ↑ + } else { + return "\u21c8"; // ⇈ + } + } + + public String slopeArrow() { + return slopeToArrowSymbol(this.calculated_value_slope * 60000); + } + + public String slopeName() { + double slope_by_minute = calculated_value_slope * 60000; + String arrow = "NONE"; + if (slope_by_minute <= (-3.5)) { + arrow = "DoubleDown"; + } else if (slope_by_minute <= (-2)) { + arrow = "SingleDown"; + } else if (slope_by_minute <= (-1)) { + arrow = "FortyFiveDown"; + } else if (slope_by_minute <= (1)) { + arrow = "Flat"; + } else if (slope_by_minute <= (2)) { + arrow = "FortyFiveUp"; + } else if (slope_by_minute <= (3.5)) { + arrow = "SingleUp"; + } else if (slope_by_minute <= (40)) { + arrow = "DoubleUp"; + } + if (hide_slope) { + arrow = "NOT COMPUTABLE"; + } + return arrow; + } + + public static String slopeName(double slope_by_minute) { + String arrow = "NONE"; + if (slope_by_minute <= (-3.5)) { + arrow = "DoubleDown"; + } else if (slope_by_minute <= (-2)) { + arrow = "SingleDown"; + } else if (slope_by_minute <= (-1)) { + arrow = "FortyFiveDown"; + } else if (slope_by_minute <= (1)) { + arrow = "Flat"; + } else if (slope_by_minute <= (2)) { + arrow = "FortyFiveUp"; + } else if (slope_by_minute <= (3.5)) { + arrow = "SingleUp"; + } else if (slope_by_minute <= (40)) { + arrow = "DoubleUp"; + } + return arrow; + } + + public static double slopefromName(String slope_name) { + if (slope_name == null) return 0; + double slope_by_minute = 0; + if (slope_name.compareTo("DoubleDown") == 0) { + slope_by_minute = -3.5; + } else if (slope_name.compareTo("SingleDown") == 0) { + slope_by_minute = -2; + } else if (slope_name.compareTo("FortyFiveDown") == 0) { + slope_by_minute = -1; + } else if (slope_name.compareTo("Flat") == 0) { + slope_by_minute = 0; + } else if (slope_name.compareTo("FortyFiveUp") == 0) { + slope_by_minute = 2; + } else if (slope_name.compareTo("SingleUp") == 0) { + slope_by_minute = 3.5; + } else if (slope_name.compareTo("DoubleUp") == 0) { + slope_by_minute = 4; + } else if (isSlopeNameInvalid(slope_name)) { + slope_by_minute = 0; + } + return slope_by_minute / 60000; + } + + public static boolean isSlopeNameInvalid(String slope_name) { + if (slope_name.compareTo("NOT_COMPUTABLE") == 0 || + slope_name.compareTo("NOT COMPUTABLE") == 0 || + slope_name.compareTo("OUT_OF_RANGE") == 0 || + slope_name.compareTo("OUT OF RANGE") == 0 || + slope_name.compareTo("NONE") == 0) { + return true; + } else { + return false; + } + } + + // Get a slope arrow based on pure guessed defaults so we can show it prior to calibration + public static String getSlopeArrowSymbolBeforeCalibration() { + final List last = BgReading.latestUnCalculated(2); + if ((last!=null) && (last.size()==2)) { + final double guess_slope = 1; // This is the "Default" slope for Dex and LimiTTer + final double time_delta = (last.get(0).timestamp-last.get(1).timestamp); + if (time_delta<=(BgGraphBuilder.DEXCOM_PERIOD * 2)) { + final double estimated_delta = (last.get(0).age_adjusted_raw_value * guess_slope) - (last.get(1).age_adjusted_raw_value * guess_slope); + final double estimated_delta2 = (last.get(0).raw_data * guess_slope) - (last.get(1).raw_data * guess_slope); + Log.d(TAG, "SlopeArrowBeforeCalibration: guess delta: " + estimated_delta + " delta2: " + estimated_delta2 + " timedelta: " + time_delta); + return slopeToArrowSymbol(estimated_delta / (time_delta / 60000)); + } else { return ""; } + } else { + return ""; + } + } + + public static boolean last_within_minutes(final int mins) { + return last_within_millis(mins * 60000); + } + + public static boolean last_within_millis(final long millis) { + final BgReading reading = last(); + return reading != null && ((JoH.tsl() - reading.timestamp) < millis); + } + + public boolean within_millis(final long millis) { + return ((JoH.tsl() - this.timestamp) < millis); + } + + public boolean isStale() { + return !within_millis(Home.stale_data_millis()); + } + + public static BgReading last() + { + return BgReading.last(Home.get_follower()); + } + + public static BgReading last(boolean is_follower) { + if (is_follower) { + return new Select() + .from(BgReading.class) + .where("calculated_value != 0") + .where("raw_data != 0") + // .where("timestamp <= ?", JoH.tsl()) + .orderBy("timestamp desc") + .executeSingle(); + } else { + Sensor sensor = Sensor.currentSensor(); + if (sensor != null) { + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("calculated_value != 0") + .where("raw_data != 0") + // .where("timestamp <= ?", JoH.tsl()) + .orderBy("timestamp desc") + .executeSingle(); + } + } + return null; + } + + public static List latest_by_size(int number) { + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) return null; + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("raw_data != 0") + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static BgReading lastNoSenssor() { + return new Select() + .from(BgReading.class) + .where("calculated_value != 0") + .where("raw_data != 0") + // .where("timestamp <= ?", JoH.tsl()) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static List latest(int number) { + return latest(number, Home.get_follower()); + } + + public static List latest(int number, boolean is_follower) { + if (is_follower) { + // exclude sensor information when working as a follower + return new Select() + .from(BgReading.class) + .where("calculated_value != 0") + .where("raw_data != 0") + // .where("timestamp <= ?", JoH.tsl()) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } else { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("calculated_value != 0") + .where("raw_data != 0") + // .where("timestamp <= ?", JoH.tsl()) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + } + + public static boolean isDataStale() { + final BgReading last = lastNoSenssor(); + if (last == null) return true; + return JoH.msSince(last.timestamp) > Home.stale_data_millis(); + } + + + public static List latestUnCalculated(int number) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { return null; } + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("raw_data != 0") + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + return new Select() + .from(BgReading.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .where("calculated_value != 0") + .where("raw_data != 0") + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List latestForGraphSensor(int number, long startTime, long endTime) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { return null; } + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .where("calculated_value != 0") + .where("raw_data != 0") + .where("calibration_uuid != \"\"") + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List latestForSensorAsc(int number, long startTime, long endTime, boolean follower) { + if (follower) { + return new Select() + .from(BgReading.class) + .where("timestamp >= ?", Math.max(startTime, 0)) + .where("timestamp <= ?", endTime) + .where("calculated_value != 0") + .where("raw_data != 0") + .orderBy("timestamp asc") + .limit(number) + .execute(); + } else { + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(BgReading.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp >= ?", Math.max(startTime, 0)) + .where("timestamp <= ?", endTime) + .where("calculated_value != 0") + .where("raw_data != 0") + .orderBy("timestamp asc") + .limit(number) + .execute(); + } + } + + public static List latestForSensorAsc(int number, long startTime, long endTime) { + return latestForSensorAsc(number, startTime, endTime, false); + } + + + public static List latestForGraphAsc(int number, long startTime) {//KS + return latestForGraphAsc(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraphAsc(int number, long startTime, long endTime) {//KS + return new Select() + .from(BgReading.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .where("calculated_value != 0") + .where("raw_data != 0") + .orderBy("timestamp asc") + .limit(number) + .execute(); + } + + public static BgReading readingNearTimeStamp(double startTime) { + final double margin = (4 * 60 * 1000); + final DecimalFormat df = new DecimalFormat("#"); + df.setMaximumFractionDigits(1); + return new Select() + .from(BgReading.class) + .where("timestamp >= " + df.format(startTime - margin)) + .where("timestamp <= " + df.format(startTime + margin)) + .where("calculated_value != 0") + .where("raw_data != 0") + .executeSingle(); + } + + public static List last30Minutes() { + double timestamp = (new Date().getTime()) - (60000 * 30); + return new Select() + .from(BgReading.class) + .where("timestamp >= " + timestamp) + .where("calculated_value != 0") + .where("raw_data != 0") + .orderBy("timestamp desc") + .execute(); + } + + public static boolean isDataSuitableForDoubleCalibration() { + final List uncalculated = BgReading.latestUnCalculated(3); + if (uncalculated.size() < 3) return false; + final ProcessInitialDataQuality.InitialDataQuality idq = ProcessInitialDataQuality.getInitialDataQuality(uncalculated); + if (!idq.pass) { + UserError.Log.d(TAG, "Data quality failure for double calibration: " + idq.advice); + } + return idq.pass || Pref.getBooleanDefaultFalse("bypass_calibration_quality_check"); + } + + + public static List futureReadings() { + double timestamp = new Date().getTime(); + return new Select() + .from(BgReading.class) + .where("timestamp > " + timestamp) + .orderBy("timestamp desc") + .execute(); + } + + // used in wear + public static BgReading findByUuid(String uuid) { + return new Select() + .from(BgReading.class) + .where("uuid = ?", uuid) + .executeSingle(); + } + + public static double estimated_bg(double timestamp) { + timestamp = timestamp + BESTOFFSET; + BgReading latest = BgReading.last(); + if (latest == null) { + return 0; + } else { + return (latest.a * timestamp * timestamp) + (latest.b * timestamp) + latest.c; + } + } + + public static double estimated_raw_bg(double timestamp) { + timestamp = timestamp + BESTOFFSET; + double estimate; + BgReading latest = BgReading.last(); + if (latest == null) { + Log.i(TAG, "No data yet, assume perfect!"); + estimate = 160; + } else { + estimate = (latest.ra * timestamp * timestamp) + (latest.rb * timestamp) + latest.rc; + } + Log.i(TAG, "ESTIMATE RAW BG" + estimate); + return estimate; + } + + public static void bgReadingInsertFromJson(String json) + { + bgReadingInsertFromJson(json, true); + } + + private static void FixCalibration(BgReading bgr) { + if (bgr.calibration_uuid == null || "".equals(bgr.calibration_uuid)) { + Log.d(TAG, "Bgr with no calibration, doing nothing"); + return; + } + Calibration calibration = Calibration.byuuid(bgr.calibration_uuid); + if (calibration == null) { + Log.i(TAG, "received Unknown calibration: " + bgr.calibration_uuid + " asking for sensor upate..."); + GcmActivity.requestSensorCalibrationsUpdate(); + } else { + bgr.calibration = calibration; + } + } + + public BgReading noRawWillBeAvailable() { + raw_data = SPECIAL_RAW_NOT_AVAILABLE; + save(); + return this; + } + + public BgReading appendSourceInfo(String info) { + if ((source_info == null) || (source_info.length() == 0)) { + source_info = info; + } else { + if (!source_info.startsWith(info) && (!source_info.contains("::" + info))) { + source_info += "::" + info; + } else { + UserError.Log.e(TAG, "Ignoring duplicate source info " + source_info + " -> " + info); + } + } + return this; + } + + public boolean isBackfilled() { + return raw_data == SPECIAL_G5_PLACEHOLDER; + } + + public boolean isRemote() { + return filtered_data == SPECIAL_REMOTE_PLACEHOLDER; + } + + public static final double SPECIAL_RAW_NOT_AVAILABLE = -0.1279; + public static final double SPECIAL_G5_PLACEHOLDER = -0.1597; + public static final double SPECIAL_FOLLOWER_PLACEHOLDER = -0.1486; + public static final double SPECIAL_REMOTE_PLACEHOLDER = -0.1375; + + public static BgReading bgReadingInsertFromG5(double calculated_value, long timestamp) { + return bgReadingInsertFromG5(calculated_value, timestamp, null); + } + + // TODO can these methods be unified to reduce duplication + // TODO remember to sync this with wear code base + public static synchronized BgReading bgReadingInsertFromG5(double calculated_value, long timestamp, String sourceInfoAppend) { + + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.w(TAG, "No sensor, ignoring this bg reading"); + return null; + } + // TODO slope!! + final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); + if (existing == null) { + final BgReading bgr = new BgReading(); + bgr.sensor = sensor; + bgr.sensor_uuid = sensor.uuid; + bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this? + bgr.timestamp = timestamp; + bgr.uuid = UUID.randomUUID().toString(); + bgr.calculated_value = calculated_value; + bgr.raw_data = SPECIAL_G5_PLACEHOLDER; // placeholder + bgr.appendSourceInfo("G5 Native"); + if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) { + bgr.appendSourceInfo(sourceInfoAppend); + } + bgr.save(); + if (JoH.ratelimit("sync wakelock", 15)) { + final PowerManager.WakeLock linger = JoH.getWakeLock("G5 Insert", 4000); + } + Inevitable.stackableTask("NotifySyncBgr", 3000, () -> notifyAndSync(bgr)); + return bgr; + } else { + return existing; + } + } + + public static synchronized BgReading bgReadingInsertMedtrum(double calculated_value, long timestamp, String sourceInfoAppend, double raw_data) { + + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.w(TAG, "No sensor, ignoring this bg reading"); + return null; + } + // TODO slope!! + final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); + if (existing == null) { + final BgReading bgr = new BgReading(); + bgr.sensor = sensor; + bgr.sensor_uuid = sensor.uuid; + bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this? + bgr.timestamp = timestamp; + bgr.uuid = UUID.randomUUID().toString(); + bgr.calculated_value = calculated_value; + bgr.raw_data = raw_data / 1000d; + bgr.filtered_data = bgr.raw_data; + if (sourceInfoAppend != null && sourceInfoAppend.equals("Backfill")) { + bgr.raw_data = BgReading.SPECIAL_G5_PLACEHOLDER; + } else { + bgr.calculateAgeAdjustedRawValue(); + } + bgr.appendSourceInfo("Medtrum Native"); + if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) { + bgr.appendSourceInfo(sourceInfoAppend); + } + bgr.save(); + if (JoH.ratelimit("sync wakelock", 15)) { + final PowerManager.WakeLock linger = JoH.getWakeLock("Medtrum Insert", 4000); + } + Inevitable.task("NotifySyncBgr" + bgr.timestamp, 3000, () -> notifyAndSync(bgr)); + if (bgr.isBackfilled()) { + handleResyncWearAfterBackfill(bgr.timestamp); + } + return bgr; + } else { + return existing; + } + } + public static synchronized BgReading bgReadingInsertLibre2(double calculated_value, long timestamp, double raw_data) { + + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.w(TAG, "No sensor, ignoring this bg reading"); + return null; + } + // TODO slope!! + final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); + if (existing == null) { + Calibration calibration = Calibration.lastValid(); + final BgReading bgReading = new BgReading(); + if (calibration == null) { + Log.d(TAG, "create: No calibration yet"); + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.raw_data = raw_data; + bgReading.age_adjusted_raw_value = raw_data; + bgReading.filtered_data = raw_data; + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + bgReading.calculated_value = calculated_value; + bgReading.calculated_value_slope = 0; + bgReading.hide_slope = false; + bgReading.appendSourceInfo("Libre2 Native"); + bgReading.find_slope(); + + bgReading.save(); + bgReading.perform_calculations(); + bgReading.postProcess(false); + + } else { + Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading); + bgReading.sensor = sensor; + bgReading.sensor_uuid = sensor.uuid; + bgReading.calibration = calibration; + bgReading.calibration_uuid = calibration.uuid; + bgReading.raw_data = raw_data ; + bgReading.age_adjusted_raw_value = raw_data; + bgReading.filtered_data = raw_data; + bgReading.timestamp = timestamp; + bgReading.uuid = UUID.randomUUID().toString(); + + bgReading.calculated_value = ((calibration.slope * calculated_value) + calibration.intercept); + bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept); + + bgReading.calculated_value_slope = 0; + bgReading.hide_slope = false; + bgReading.appendSourceInfo("Libre2 Native"); + + BgReading.updateCalculatedValueToWithinMinMax(bgReading); + + bgReading.find_slope(); + bgReading.save(); + + bgReading.postProcess(false); + + } + + return bgReading; + } else { + return existing; + } + } + + public static void handleResyncWearAfterBackfill(final long earliest) { + if (earliest_backfill == 0 || earliest < earliest_backfill) earliest_backfill = earliest; + if (WatchUpdaterService.isEnabled()) { + Inevitable.task("wear-backfill-sync", 10000, () -> { + WatchUpdaterService.startServiceAndResendDataIfNeeded(earliest_backfill); + earliest_backfill = 0; + }); + } + } + + public void setRemoteMarker() { + filtered_data = SPECIAL_REMOTE_PLACEHOLDER; + } + + + public static void notifyAndSync(final BgReading bgr) { + final boolean recent = bgr.isCurrent(); + if (recent) { + Notifications.start(); // may not be needed as this is duplicated in handleNewBgReading + // probably not wanted for G5 internal values? + //bgr.injectNoise(true); // Add noise parameter for nightscout + //bgr.injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout + } + BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower(), !recent); // pebble and widget and follower + } + + public static BgReading bgReadingInsertFromJson(String json, boolean do_notification) { + return bgReadingInsertFromJson(json, do_notification, WholeHouse.isEnabled()); + } + + public static BgReading bgReadingInsertFromJson(String json, boolean do_notification, boolean force_sensor) { + if ((json == null) || (json.length() == 0)) { + Log.e(TAG, "bgreadinginsertfromjson passed a null or zero length json"); + return null; + } + final BgReading bgr = fromJSON(json); + if (bgr != null) { + try { + if (readingNearTimeStamp(bgr.timestamp) == null) { + FixCalibration(bgr); + if (force_sensor) { + final Sensor forced_sensor = Sensor.currentSensor(); + if (forced_sensor != null) { + bgr.sensor = forced_sensor; + bgr.sensor_uuid = forced_sensor.uuid; + } + if (Pref.getBooleanDefaultFalse("illustrate_remote_data")) { + bgr.setRemoteMarker(); + } + } + final long now = JoH.tsl(); + if (bgr.timestamp > now) { + UserError.Log.wtf(TAG, "Received a bg reading that appears to be in the future: " + JoH.dateTimeText(bgr.timestamp) + " vs " + JoH.dateTimeText(now)); + } + bgr.save(); + if (do_notification) { + Notifications.start(); // this may not be needed as it fires in handleNewBgReading + //xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al + BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower()); // pebble and widget and follower + } + } else { + Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + json); + } + } catch (Exception e) { + Log.d(TAG, "Could not save BGR: " + e.toString()); + } + } else { + Log.e(TAG,"Got null bgr from json"); + } + return bgr; + } + + // TODO this method shares some code with above.. merge + public static void bgReadingInsertFromInt(int value, long timestamp, boolean do_notification) { + // TODO sanity check data! + + if ((value <= 0) || (timestamp <= 0)) { + Log.e(TAG, "Invalid data fed to InsertFromInt"); + return; + } + + BgReading bgr = new BgReading(); + + if (bgr != null) { + bgr.uuid = UUID.randomUUID().toString(); + + bgr.timestamp = timestamp; + bgr.calculated_value = value; + + + // rough code for testing! + bgr.filtered_calculated_value = value; + bgr.raw_data = value; + bgr.age_adjusted_raw_value = value; + bgr.filtered_data = value; + + final Sensor forced_sensor = Sensor.currentSensor(); + if (forced_sensor != null) { + bgr.sensor = forced_sensor; + bgr.sensor_uuid = forced_sensor.uuid; + } + + try { + if (readingNearTimeStamp(bgr.timestamp) == null) { + bgr.save(); + bgr.find_slope(); + if (do_notification) { + // xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al + Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading + } + BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), false, !do_notification); // pebble and widget + } else { + Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + timestamp); + } + } catch (Exception e) { + Log.d(TAG, "Could not save BGR: " + e.toString()); + } + } else { + Log.e(TAG,"Got null bgr from create"); + } + } + + public static BgReading byUUID(String uuid) { + if (uuid == null) return null; + return new Select() + .from(BgReading.class) + .where("uuid = ?", uuid) + .executeSingle(); + } + + public static BgReading byid(long id) { + return new Select() + .from(BgReading.class) + .where("_ID = ?", id) + .executeSingle(); + } + + public static BgReading fromJSON(String json) { + if (json.length()==0) + { + Log.d(TAG,"Empty json received in bgreading fromJson"); + return null; + } + try { + Log.d(TAG, "Processing incoming json: " + json); + return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json,BgReading.class); + } catch (Exception e) { + Log.d(TAG, "Got exception parsing BgReading json: " + e.toString()); + Home.toaststaticnext("Error on BGReading sync, probably decryption key mismatch"); + return null; + } + } + + private BgReadingMessage toMessageNative() { + return new BgReadingMessage.Builder() + .timestamp(timestamp) + //.a(a) + //.b(b) + //.c(c) + .age_adjusted_raw_value(age_adjusted_raw_value) + .calculated_value(calculated_value) + .filtered_calculated_value(filtered_calculated_value) + .calibration_flag(calibration_flag) + .raw_calculated(raw_calculated) + .raw_data(raw_data) + .calculated_value_slope(calculated_value_slope) + //.calibration_uuid(calibration_uuid) + .uuid(uuid) + .build(); + } + + public byte[] toMessage() { + final List btl = new ArrayList<>(); + btl.add(this); + return toMultiMessage(btl); + } + + public static byte[] toMultiMessage(List bgl) { + if (bgl == null) return null; + final List BgReadingMessageList = new ArrayList<>(); + for (BgReading bg : bgl) { + BgReadingMessageList.add(bg.toMessageNative()); + } + return BgReadingMultiMessage.ADAPTER.encode(new BgReadingMultiMessage(BgReadingMessageList)); + } + + private static final long CLOSEST_READING_MS = 290000; + private static void processFromMessage(BgReadingMessage btm) { + if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) { + BgReading bg = byUUID(btm.uuid); + if (bg != null) { + // we already have this uuid and we don't have a circumstance to update the record, so quick return here + return; + } + if (bg == null) { + bg = getForPreciseTimestamp(Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS, false); + if (bg != null) { + UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bg.uuid + " vs " + btm.uuid + " skipping!"); + return; + } + bg = new BgReading(); + } + + bg.timestamp = Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP); + bg.calculated_value = Wire.get(btm.calculated_value, BgReadingMessage.DEFAULT_CALCULATED_VALUE); + bg.filtered_calculated_value = Wire.get(btm.filtered_calculated_value, BgReadingMessage.DEFAULT_FILTERED_CALCULATED_VALUE); + bg.calibration_flag = Wire.get(btm.calibration_flag, BgReadingMessage.DEFAULT_CALIBRATION_FLAG); + bg.raw_calculated = Wire.get(btm.raw_calculated, BgReadingMessage.DEFAULT_RAW_CALCULATED); + bg.raw_data = Wire.get(btm.raw_data, BgReadingMessage.DEFAULT_RAW_DATA); + bg.calculated_value_slope = Wire.get(btm.calculated_value_slope, BgReadingMessage.DEFAULT_CALCULATED_VALUE_SLOPE); + bg.calibration_uuid = btm.calibration_uuid; + bg.uuid = btm.uuid; + bg.save(); + } else { + UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid"); + } + } + + public synchronized static void processFromMultiMessage(byte[] payload) { + try { + final BgReadingMultiMessage bgmm = BgReadingMultiMessage.ADAPTER.decode(payload); + if ((bgmm != null) && (bgmm.bgreading_message != null)) { + for (BgReadingMessage btm : bgmm.bgreading_message) { + processFromMessage(btm); + } + Home.staticRefreshBGCharts(); + } + } catch (IOException | NullPointerException | IllegalStateException e) { + UserError.Log.e(TAG, "exception processFromMessage: " + e); + } + } + + public String toJSON(boolean sendCalibration) { + final JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("uuid", uuid); + jsonObject.put("a", a); // how much of this do we actually need? + jsonObject.put("b", b); + jsonObject.put("c", c); + jsonObject.put("timestamp", timestamp); + jsonObject.put("age_adjusted_raw_value", age_adjusted_raw_value); + jsonObject.put("calculated_value", calculated_value); + jsonObject.put("filtered_calculated_value", filtered_calculated_value); + jsonObject.put("calibration_flag", calibration_flag); + jsonObject.put("filtered_data", filtered_data); + jsonObject.put("raw_calculated", raw_calculated); + jsonObject.put("raw_data", raw_data); + try { + jsonObject.put("calculated_value_slope", calculated_value_slope); + } catch (JSONException e) { + jsonObject.put("hide_slope", true); // calculated value slope is NaN - hide slope should already be true locally too + } + if (sendCalibration) { + jsonObject.put("calibration_uuid", calibration_uuid); + } + // jsonObject.put("sensor", sensor); + return jsonObject.toString(); + } catch (JSONException e) { + UserError.Log.wtf(TAG, "Error producing in toJSON: " + e); + if (Double.isNaN(a)) Log.e(TAG, "a is NaN"); + if (Double.isNaN(b)) Log.e(TAG, "b is NaN"); + if (Double.isNaN(c)) Log.e(TAG, "c is NaN"); + if (Double.isNaN(age_adjusted_raw_value)) Log.e(TAG, "age_adjusted_raw_value is NaN"); + if (Double.isNaN(calculated_value)) Log.e(TAG, "calculated_value is NaN"); + if (Double.isNaN(filtered_calculated_value)) Log.e(TAG, "filtered_calculated_value is NaN"); + if (Double.isNaN(filtered_data)) Log.e(TAG, "filtered_data is NaN"); + if (Double.isNaN(raw_calculated)) Log.e(TAG, "raw_calculated is NaN"); + if (Double.isNaN(raw_data)) Log.e(TAG, "raw_data is NaN"); + if (Double.isNaN(calculated_value_slope)) Log.e(TAG, "calculated_value_slope is NaN"); + return ""; + } + } + + public static void deleteALL() { + try { + SQLiteUtils.execSql("delete from BgSendQueue"); + SQLiteUtils.execSql("delete from BgReadings"); + Log.d(TAG, "Deleting all BGReadings"); + } catch (Exception e) { + Log.e(TAG, "Got exception running deleteALL " + e.toString()); + } + } + + public static void deleteRandomData() { + Random rand = new Random(); + int minutes_ago_end = rand.nextInt(120); + int minutes_ago_start = minutes_ago_end + rand.nextInt(35)+5; + long ts_start = JoH.tsl() - minutes_ago_start * Constants.MINUTE_IN_MS; + long ts_end = JoH.tsl() - minutes_ago_end * Constants.MINUTE_IN_MS; + UserError.Log.d(TAG,"Deleting random bgreadings: "+JoH.dateTimeText(ts_start)+" -> "+JoH.dateTimeText(ts_end)); + testDeleteRange(ts_start, ts_end); + } + + public static void testDeleteRange(long start_time, long end_time) { + List bgrs = new Delete() + .from(BgReading.class) + .where("timestamp < ?", end_time) + .where("timestamp > ?",start_time) + .execute(); + // UserError.Log.d("OB1TEST","Deleted: "+bgrs.size()+" records"); + } + + public static List cleanup(int retention_days) { + return new Delete() + .from(BgReading.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS)) + .execute(); + } + + public static void cleanupOutOfRangeValues() { + new Delete() + .from(BgReading.class) + .where("timestamp > ?", JoH.tsl() - (3 * Constants.DAY_IN_MS)) + .where("calculated_value > ?", 324) + .execute(); + } + + + // used in wear + public static void cleanup(long timestamp) { + try { + SQLiteUtils.execSql("delete from BgSendQueue"); + List data = new Select() + .from(BgReading.class) + .where("timestamp < ?", timestamp) + .orderBy("timestamp desc") + .execute(); + if (data != null) Log.d(TAG, "cleanup BgReading size=" + data.size()); + new Cleanup().execute(data); + } catch (Exception e) { + Log.e(TAG, "Got exception running cleanup " + e.toString()); + } + } + + // used in wear + private static class Cleanup extends AsyncTask, Integer, Boolean> { + @Override + protected Boolean doInBackground(List... errors) { + try { + for(BgReading data : errors[0]) { + data.delete(); + } + return true; + } catch(Exception e) { + return false; + } + } + } + + + //*******INSTANCE METHODS***********// + public void perform_calculations() { + find_new_curve(); + find_new_raw_curve(); + find_slope(); + } + + public void find_slope() { + List last_2 = BgReading.latest(2); + + // FYI: By default, assertions are disabled at runtime. Add "-ea" to commandline to enable. + // https://docs.oracle.com/javase/7/docs/technotes/guides/language/assert.html + assert last_2.get(0).uuid.equals(this.uuid) + : "Invariant condition not fulfilled: calculating slope and current reading wasn't saved before"; + + if ((last_2 != null) && (last_2.size() == 2)) { + calculated_value_slope = calculateSlope(this, last_2.get(1)); + save(); + } else if ((last_2 != null) && (last_2.size() == 1)) { + calculated_value_slope = 0; + save(); + } else { + if (JoH.ratelimit("no-bg-couldnt-find-slope", 15)) { + Log.w(TAG, "NO BG? COULDNT FIND SLOPE!"); + } + } + } + + + public void find_new_curve() { + JoH.clearCache(); + List last_3 = BgReading.latest(3); + if ((last_3 != null) && (last_3.size() == 3)) { + BgReading latest = last_3.get(0); + BgReading second_latest = last_3.get(1); + BgReading third_latest = last_3.get(2); + + double y3 = latest.calculated_value; + double x3 = latest.timestamp; + double y2 = second_latest.calculated_value; + double x2 = second_latest.timestamp; + double y1 = third_latest.calculated_value; + double x1 = third_latest.timestamp; + + a = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2)); + b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2))); + c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2))); + + Log.i(TAG, "find_new_curve: BG PARABOLIC RATES: "+a+"x^2 + "+b+"x + "+c); + + save(); + } else if ((last_3 != null) && (last_3.size() == 2)) { + + Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume Linear"); + BgReading latest = last_3.get(0); + BgReading second_latest = last_3.get(1); + + double y2 = latest.calculated_value; + double x2 = latest.timestamp; + double y1 = second_latest.calculated_value; + double x1 = second_latest.timestamp; + + if(y1 == y2) { + b = 0; + } else { + b = (y2 - y1)/(x2 - x1); + } + a = 0; + c = -1 * ((latest.b * x1) - y1); + + Log.i(TAG, ""+latest.a+"x^2 + "+latest.b+"x + "+latest.c); + save(); + } else { + Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume static data"); + a = 0; + b = 0; + c = calculated_value; + + Log.i(TAG, ""+a+"x^2 + "+b+"x + "+c); + save(); + } + } + + public void calculateAgeAdjustedRawValue(){ + final double adjust_for = AGE_ADJUSTMENT_TIME - time_since_sensor_started; + if ((adjust_for > 0) && (!DexCollectionType.hasLibre())) { + age_adjusted_raw_value = ((AGE_ADJUSTMENT_FACTOR * (adjust_for / AGE_ADJUSTMENT_TIME)) * raw_data) + raw_data; + Log.i(TAG, "calculateAgeAdjustedRawValue: RAW VALUE ADJUSTMENT FROM:" + raw_data + " TO: " + age_adjusted_raw_value); + } else { + age_adjusted_raw_value = raw_data; + } + } + + void find_new_raw_curve() { + JoH.clearCache(); + final List last_3 = BgReading.latest(3); + if ((last_3 != null) && (last_3.size() == 3)) { + + final BgReading latest = last_3.get(0); + final BgReading second_latest = last_3.get(1); + final BgReading third_latest = last_3.get(2); + + double y3 = latest.age_adjusted_raw_value; + double x3 = latest.timestamp; + double y2 = second_latest.age_adjusted_raw_value; + double x2 = second_latest.timestamp; + double y1 = third_latest.age_adjusted_raw_value; + double x1 = third_latest.timestamp; + + ra = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2)); + rb = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2))); + rc = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2))); + + Log.i(TAG, "find_new_raw_curve: RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc); + save(); + } else if ((last_3 != null) && (last_3.size()) == 2) { + BgReading latest = last_3.get(0); + BgReading second_latest = last_3.get(1); + + double y2 = latest.age_adjusted_raw_value; + double x2 = latest.timestamp; + double y1 = second_latest.age_adjusted_raw_value; + double x1 = second_latest.timestamp; + if(y1 == y2) { + rb = 0; + } else { + rb = (y2 - y1)/(x2 - x1); + } + ra = 0; + rc = -1 * ((latest.rb * x1) - y1); + + Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume Linear data"); + + Log.i(TAG, "RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc); + save(); + } else { + Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume static data"); + BgReading latest_entry = BgReading.lastNoSenssor(); + ra = 0; + rb = 0; + if (latest_entry != null) { + rc = latest_entry.age_adjusted_raw_value; + } else { + rc = 105; + } + + save(); + } + } + private static double weightedAverageRaw(double timeA, double timeB, double calibrationTime, double rawA, double rawB) { + final double relativeSlope = (rawB - rawA)/(timeB - timeA); + final double relativeIntercept = rawA - (relativeSlope * timeA); + return ((relativeSlope * calibrationTime) + relativeIntercept); + } + + public String toS() { + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + return gson.toJson(this); + } + + public String timeStamp() { + return JoH.dateTimeText(timestamp); + } + + public int noiseValue() { + if(noise == null || noise.compareTo("") == 0) { + return 1; + } else { + return Integer.valueOf(noise); + } + } + + public BgReading injectNoise(boolean save) { + final BgReading bgReading = this; + if (JoH.msSince(bgReading.timestamp) > Constants.MINUTE_IN_MS * 20) { + bgReading.noise = "0"; + } else { + BgGraphBuilder.refreshNoiseIfOlderThan(bgReading.timestamp); + if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_HIGH) { + bgReading.noise = "4"; + } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TOO_HIGH_FOR_PREDICT) { + bgReading.noise = "3"; + } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TRIGGER) { + bgReading.noise = "2"; + } + } + if (save) bgReading.save(); + return bgReading; + } + + // list(0) is the most recent reading. + public static List getXRecentPoints(int NumReadings) { + List latest = BgReading.latest(NumReadings); + if (latest == null || latest.size() != NumReadings) { + // for less than NumReadings readings, we can't tell what the situation + // + Log.d(TAG_ALERT, "getXRecentPoints we don't have enough readings, returning null"); + return null; + } + // So, we have at least three values... + for(BgReading bgReading : latest) { + Log.d(TAG_ALERT, "getXRecentPoints - reading: time = " + bgReading.timestamp + " calculated_value " + bgReading.calculated_value); + } + + // now let's check that they are relevant. the last reading should be from the last 5 minutes, + // x-1 more readings should be from the last (x-1)*5 minutes. we will allow 5 minutes for the last + // x to allow one packet to be missed. + if (new Date().getTime() - latest.get(NumReadings - 1).timestamp > (NumReadings * 5 + 6) * 60 * 1000) { + Log.d(TAG_ALERT, "getXRecentPoints we don't have enough points from the last " + (NumReadings * 5 + 6) + " minutes, returning null"); + return null; + } + return latest; + + } + + public static boolean checkForPersistentHigh() { + + // skip if not enabled + if (!Pref.getBooleanDefaultFalse("persistent_high_alert_enabled")) return false; + + + List last = BgReading.latest(1); + if ((last != null) && (last.size()>0)) { + + final long now = JoH.tsl(); + final long since = now - last.get(0).timestamp; + // only process if last reading <10 mins + if (since < 600000) { + // check if exceeding high + if (last.get(0).calculated_value > + Home.convertToMgDlIfMmol( + JoH.tolerantParseDouble(Pref.getString("highValue", "170")))) { + + final double this_slope = last.get(0).calculated_value_slope * 60000; + //Log.d(TAG, "CheckForPersistentHigh: Slope: " + JoH.qs(this_slope)); + + // if not falling + if (this_slope > 0) { + final long high_since = Pref.getLong(PERSISTENT_HIGH_SINCE, 0); + if (high_since == 0) { + // no previous persistent high so set start as now + Pref.setLong(PERSISTENT_HIGH_SINCE, now); + Log.d(TAG, "Registering start of persistent high at time now"); + } else { + final long high_for_mins = (now - high_since) / (1000 * 60); + long threshold_mins; + try { + threshold_mins = Long.parseLong(Pref.getString("persistent_high_threshold_mins", "60")); + } catch (NumberFormatException e) { + threshold_mins = 60; + Home.toaststaticnext("Invalid persistent high for longer than minutes setting: using 60 mins instead"); + } + if (high_for_mins > threshold_mins) { + // we have been high for longer than the threshold - raise alert + + // except if alerts are disabled + if (Pref.getLong("alerts_disabled_until", 0) > new Date().getTime()) { + Log.i(TAG, "checkforPersistentHigh: Notifications are currently disabled cannot alert!!"); + return false; + } + Log.i(TAG, "Persistent high for: " + high_for_mins + " mins -> alerting"); + Notifications.persistentHighAlert(xdrip.getAppContext(), true, xdrip.getAppContext().getString(R.string.persistent_high_for_greater_than) + (int) high_for_mins + xdrip.getAppContext().getString(R.string.space_mins)); + + } else { + Log.d(TAG, "Persistent high below time threshold at: " + high_for_mins); + } + } + } + } else { + // not high - cancel any existing + if (Pref.getLong(PERSISTENT_HIGH_SINCE,0)!=0) + { + Log.i(TAG,"Cancelling previous persistent high as we are no longer high"); + Pref.setLong(PERSISTENT_HIGH_SINCE, 0); // clear it + Notifications.persistentHighAlert(xdrip.getAppContext(), false, ""); // cancel it + } + } + } + } + return false; // actually we should probably return void as we do everything inside this method + } + + public static void checkForRisingAllert(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Boolean rising_alert = prefs.getBoolean("rising_alert", false); + if(!rising_alert) { + return; + } + if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ + Log.i("NOTIFICATIONS", "checkForRisingAllert: Notifications are currently disabled!!"); + return; + } + + String riseRate = prefs.getString("rising_bg_val", "2"); + float friseRate = 2; + + try + { + friseRate = Float.parseFloat(riseRate); + } + catch (NumberFormatException nfe) + { + Log.e(TAG_ALERT, "checkForRisingAllert reading falling_bg_val failed, continuing with 2", nfe); + } + Log.d(TAG_ALERT, "checkForRisingAllert will check for rate of " + friseRate); + + boolean riseAlert = checkForDropRiseAllert(friseRate, false); + Notifications.RisingAlert(context, riseAlert); + } + + + public static void checkForDropAllert(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Boolean falling_alert = prefs.getBoolean("falling_alert", false); + if(!falling_alert) { + return; + } + if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ + Log.d("NOTIFICATIONS", "checkForDropAllert: Notifications are currently disabled!!"); + return; + } + + String dropRate = prefs.getString("falling_bg_val", "2"); + float fdropRate = 2; + + try + { + fdropRate = Float.parseFloat(dropRate); + } + catch (NumberFormatException nfe) + { + Log.e(TAG_ALERT, "reading falling_bg_val failed, continuing with 2", nfe); + } + Log.i(TAG_ALERT, "checkForDropAllert will check for rate of " + fdropRate); + + boolean dropAlert = checkForDropRiseAllert(fdropRate, true); + Notifications.DropAlert(context, dropAlert); + } + + // true say, alert is on. + private static boolean checkForDropRiseAllert(float MaxSpeed, boolean drop) { + Log.d(TAG_ALERT, "checkForDropRiseAllert called drop=" + drop); + List latest = getXRecentPoints(4); + if(latest == null) { + Log.d(TAG_ALERT, "checkForDropRiseAllert we don't have enough points from the last 15 minutes, returning false"); + return false; + } + float time3 = (latest.get(0).timestamp - latest.get(3).timestamp) / 60000; + double bg_diff3 = latest.get(3).calculated_value - latest.get(0).calculated_value; + if (!drop) { + bg_diff3 *= (-1); + } + Log.i(TAG_ALERT, "bg_diff3=" + bg_diff3 + " time3 = " + time3); + if(bg_diff3 < time3 * MaxSpeed) { + Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 4 points not fast enough, returning false"); + return false; + } + // we should alert here, but if the last measurement was less than MaxSpeed / 2, I won't. + + + float time1 = (latest.get(0).timestamp - latest.get(1).timestamp) / 60000; + double bg_diff1 = latest.get(1).calculated_value - latest.get(0).calculated_value; + if (!drop) { + bg_diff1 *= (-1); + } + + if(time1 > 7.0) { + Log.d(TAG_ALERT, "checkForDropRiseAllert the two points are not close enough, returning true"); + return true; + } + if(bg_diff1 < time1 * MaxSpeed /2) { + Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 2 points not fast enough, returning false"); + return false; + } + Log.d(TAG_ALERT, "checkForDropRiseAllert returning true speed is " + (bg_diff3 / time3)); + return true; + } + + // Make sure that this function either sets the alert or removes it. + public static boolean getAndRaiseUnclearReading(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ + Log.d("NOTIFICATIONS", "getAndRaiseUnclearReading Notifications are currently disabled!!"); + UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); + return false; + } + + Boolean bg_unclear_readings_alerts = prefs.getBoolean("bg_unclear_readings_alerts", false); + if (!bg_unclear_readings_alerts + || !DexCollectionType.hasFiltered() + || Ob1G5CollectionService.usingG6() + || Ob1G5CollectionService.usingNativeMode()) { + Log.d(TAG_ALERT, "getUnclearReading returned false since feature is disabled"); + UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); + return false; + } + Long UnclearTimeSetting = Long.parseLong(prefs.getString("bg_unclear_readings_minutes", "90")) * 60000; + + Long UnclearTime = BgReading.getUnclearTime(UnclearTimeSetting); + + if (UnclearTime >= UnclearTimeSetting ) { + Log.d("NOTIFICATIONS", "Readings have been unclear for too long!!"); + Notifications.bgUnclearAlert(context); + return true; + } + + UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); + + if (UnclearTime > 0 ) { + Log.d(TAG_ALERT, "We are in an clear state, but not for too long. Alerts are disabled"); + return true; + } + + return false; + } + /* + * This function comes to check weather we are in a case that we have an allert but since things are + * getting better we should not do anything. (This is only in the case that the alert was snoozed before.) + * This means that if this is a low alert, and we have two readings in the last 15 minutes, and + * either we have gone in 10 in the last two readings, or we have gone in 3 in the last reading, we + * don't play the alert again, but rather wait for the alert to finish. + * I'll start with having the same values for the high alerts. + */ + + public static boolean trendingToAlertEnd(Context context, boolean above) { + // TODO: check if we are not in an UnclerTime. + Log.d(TAG_ALERT, "trendingToAlertEnd called"); + + List latest = getXRecentPoints(3); + if(latest == null) { + Log.d(TAG_ALERT, "trendingToAlertEnd we don't have enough points from the last 15 minutes, returning false"); + return false; + } + + if(above == false) { + // This is a low alert, we should be going up + if((latest.get(0).calculated_value - latest.get(1).calculated_value > 4) || + (latest.get(0).calculated_value - latest.get(2).calculated_value > 10)) { + Log.d(TAG_ALERT, "trendingToAlertEnd returning true for low alert"); + return true; + } + } else { + // This is a high alert we should be heading down + if((latest.get(1).calculated_value - latest.get(0).calculated_value > 4) || + (latest.get(2).calculated_value - latest.get(0).calculated_value > 10)) { + Log.d(TAG_ALERT, "trendingToAlertEnd returning true for high alert"); + return true; + } + } + Log.d(TAG_ALERT, "trendingToAlertEnd returning false, not in the right direction (or not fast enough)"); + return false; + + } + + // Should that be combined with noiseValue? + private Boolean Unclear() { + Log.d(TAG_ALERT, "Unclear filtered_data=" + filtered_data + " raw_data=" + raw_data); + return raw_data > filtered_data * 1.3 || raw_data < filtered_data * 0.7; + } + + /* + * returns the time (in ms) that the state is not clear and no alerts should work + * The base of the algorithm is that any period can be bad or not. bgReading.Unclear() tells that. + * a non clear bgReading means MAX_INFLUANCE time after it we are in a bad position + * Since this code is based on heuristics, and since times are not accurate, boundary issues can be ignored. + * + * interstingTime is the period to check. That is if the last period is bad, we want to know how long does it go bad... + * */ + + // The extra 120,000 is to allow the packet to be delayed for some time and still be counted in that group + // Please don't use for MAX_INFLUANCE a number that is complete multiply of 5 minutes (300,000) + static final int MAX_INFLUANCE = 30 * 60000 - 120000; // A bad point means data is untrusted for 30 minutes. + private static Long getUnclearTimeHelper(List latest, Long interstingTime, final Long now) { + + // The code ignores missing points (that is they some times are treated as good and some times as bad. + // If this bothers someone, I believe that the list should be filled with the missing points as good and continue to run. + + Long LastGoodTime = 0l; // 0 represents that we are not in a good part + + Long UnclearTime = 0l; + for(BgReading bgReading : latest) { + // going over the readings from latest to first + if(bgReading.timestamp < now - (interstingTime + MAX_INFLUANCE)) { + // Some readings are missing, we can stop checking + break; + } + if(bgReading.timestamp <= now - MAX_INFLUANCE && UnclearTime == 0) { + Log.d(TAG_ALERT, "We did not have a problematic reading for MAX_INFLUANCE time, so now all is well"); + return 0l; + + } + if (bgReading.Unclear()) { + // here we assume that there are no missing points. Missing points might join the good and bad values as well... + // we should have checked if we have a period, but it is hard to say how to react to them. + Log.d(TAG_ALERT, "We have a bad reading, so setting UnclearTime to " + bgReading.timestamp); + UnclearTime = bgReading.timestamp; + LastGoodTime = 0l; + } else { + if (LastGoodTime == 0l) { + Log.d(TAG_ALERT, "We are starting a good period at "+ bgReading.timestamp); + LastGoodTime = bgReading.timestamp; + } else { + // we have some good period, is it good enough? + if(LastGoodTime - bgReading.timestamp >= MAX_INFLUANCE) { + // Here UnclearTime should be already set, otherwise we will return a toob big value + if (UnclearTime ==0) { + Log.wtf(TAG_ALERT, "ERROR - UnclearTime must not be 0 here !!!"); + } + Log.d(TAG_ALERT, "We have a good period from " + bgReading.timestamp + " to " + LastGoodTime + "returning " + (now - UnclearTime +5 *60000)); + return now - UnclearTime + 5 *60000; + } + } + } + } + // if we are here, we have a problem... or not. + if(UnclearTime == 0l) { + Log.d(TAG_ALERT, "Since we did not find a good period, but we also did not find a single bad value, we assume things are good"); + return 0l; + } + Log.d(TAG_ALERT, "We scanned all over, but could not find a good period. we have a bad value, so assuming that the whole period is bad" + + " returning " + interstingTime); + // Note that we might now have all the points, and in this case, since we don't have a good period I return a bad period. + return interstingTime; + + } + + // This is to enable testing of the function, by passing different values + public static Long getUnclearTime(Long interstingTime) { + List latest = BgReading.latest((interstingTime.intValue() + MAX_INFLUANCE)/ 60000 /5 ); + if (latest == null) { + return 0L; + } + final Long now = new Date().getTime(); + return getUnclearTimeHelper(latest, interstingTime, now); + + } + + public static Long getTimeSinceLastReading() { + BgReading bgReading = BgReading.last(); + if (bgReading != null) { + return (new Date().getTime() - bgReading.timestamp); + } + return (long) 0; + } + + public double usedRaw() { + Calibration calibration = Calibration.lastValid(); + if (calibration != null && calibration.check_in) { + return raw_data; + } + return age_adjusted_raw_value; + } + + public boolean isCurrent() { + return JoH.msSince(timestamp) < Constants.MINUTE_IN_MS * 2; + } + + public double ageAdjustedFiltered(){ + double usedRaw = usedRaw(); + if(usedRaw == raw_data || raw_data == 0d){ + return filtered_data; + } else { + // adjust the filtered_data with the same factor as the age adjusted raw value + return filtered_data * (usedRaw/raw_data); + } + } + + // ignores calibration checkins for speed + public double ageAdjustedFiltered_fast() { + // adjust the filtered_data with the same factor as the age adjusted raw value + return filtered_data * (age_adjusted_raw_value / raw_data); + } + + // the input of this function is a string. each char can be g(=good) or b(=bad) or s(=skip, point unmissed). + static List createlatestTest(String input, Long now) { + Random randomGenerator = new Random(); + List out = new LinkedList (); + char[] chars= input.toCharArray(); + for(int i=0; i < chars.length; i++) { + BgReading bg = new BgReading(); + int rand = randomGenerator.nextInt(20000) - 10000; + bg.timestamp = now - i * 5 * 60000 + rand; + bg.raw_data = 150; + if(chars[i] == 'g') { + bg.filtered_data = 151; + } else if (chars[i] == 'b') { + bg.filtered_data = 110; + } else { + continue; + } + out.add(bg); + } + return out; + + + } + static void TestgetUnclearTime(String input, Long interstingTime, Long expectedResult) { + final Long now = new Date().getTime(); + List readings = createlatestTest(input, now); + Long result = getUnclearTimeHelper(readings, interstingTime * 60000, now); + if (result >= expectedResult * 60000 - 20000 && result <= expectedResult * 60000+20000) { + Log.d(TAG_ALERT, "Test passed"); + } else { + Log.d(TAG_ALERT, "Test failed expectedResult = " + expectedResult + " result = "+ result / 60000.0); + } + + } + + public static void TestgetUnclearTimes() { + TestgetUnclearTime("gggggggggggggggggggggggg", 90l, 0l * 5); + TestgetUnclearTime("bggggggggggggggggggggggg", 90l, 1l * 5); + TestgetUnclearTime("bbgggggggggggggggggggggg", 90l, 2l *5 ); + TestgetUnclearTime("gbgggggggggggggggggggggg", 90l, 2l * 5); + TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 90l, 18l * 5); + TestgetUnclearTime("bbbgggggggbbgggggggggggg", 90l, 3l * 5); + TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 90l, 0l * 5); + TestgetUnclearTime("ggssgggggggggggggggggggg", 90l, 0l * 5); + TestgetUnclearTime("ggssbggssggggggggggggggg", 90l, 5l * 5); + TestgetUnclearTime("bb", 90l, 18l * 5); + + // intersting time is 2 minutes, we should always get 0 (in 5 minutes units + TestgetUnclearTime("gggggggggggggggggggggggg", 2l, 0l * 5); + TestgetUnclearTime("bggggggggggggggggggggggg", 2l, 2l); + TestgetUnclearTime("bbgggggggggggggggggggggg", 2l, 2l); + TestgetUnclearTime("gbgggggggggggggggggggggg", 2l, 2l); + TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 2l, 2l); + + // intersting time is 10 minutes, we should always get 0 (in 5 minutes units + TestgetUnclearTime("gggggggggggggggggggggggg", 10l, 0l * 5); + TestgetUnclearTime("bggggggggggggggggggggggg", 10l, 1l * 5); + TestgetUnclearTime("bbgggggggggggggggggggggg", 10l, 2l * 5); + TestgetUnclearTime("gbgggggggggggggggggggggg", 10l, 2l * 5); + TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 10l, 2l * 5); + TestgetUnclearTime("bbbgggggggbbgggggggggggg", 10l, 2l * 5); + TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 10l, 0l * 5); + TestgetUnclearTime("ggssgggggggggggggggggggg", 10l, 0l * 5); + TestgetUnclearTime("ggssbggssggggggggggggggg", 10l, 2l * 5); + TestgetUnclearTime("bb", 10l, 2l * 5); + } + + public int getSlopeOrdinal() { + double slope_by_minute = calculated_value_slope * 60000; + int ordinal = 0; + if(!hide_slope) { + if (slope_by_minute <= (-3.5)) { + ordinal = 7; + } else if (slope_by_minute <= (-2)) { + ordinal = 6; + } else if (slope_by_minute <= (-1)) { + ordinal = 5; + } else if (slope_by_minute <= (1)) { + ordinal = 4; + } else if (slope_by_minute <= (2)) { + ordinal = 3; + } else if (slope_by_minute <= (3.5)) { + ordinal = 2; + } else { + ordinal = 1; + } + } + return ordinal; + } + + public int getMgdlValue() { + return (int) calculated_value; + } + + public long getEpochTimestamp() { + return timestamp; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/BloodTest.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/BloodTest.java new file mode 100644 index 0000000..ba5ba73 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/BloodTest.java @@ -0,0 +1,577 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; +import android.util.Log; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.AddCalibration; +import com.eveningoutpost.dexdrip.GlucoseMeter.GlucoseReadingRx; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.Services.SyncService; +import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue; +import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract; +import com.eveningoutpost.dexdrip.calibrations.NativeCalibrationPipe; +import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration; +import com.eveningoutpost.dexdrip.messages.BloodTestMessage; +import com.eveningoutpost.dexdrip.messages.BloodTestMultiMessage; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.common.math.DoubleMath; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.squareup.wire.Wire; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * Created by jamorham on 11/12/2016. + */ + +@Table(name = "BloodTest", id = BaseColumns._ID) +public class BloodTest extends Model { + + public static final long STATE_VALID = 1 << 0; + public static final long STATE_CALIBRATION = 1 << 1; + public static final long STATE_NOTE = 1 << 2; + public static final long STATE_UNDONE = 1 << 3; + public static final long STATE_OVERWRITTEN = 1 << 4; + + private static long highest_timestamp = 0; + private static boolean patched = false; + private final static String TAG = "BloodTest"; + private final static String LAST_BT_AUTO_CALIB_UUID = "last-bt-auto-calib-uuid"; + private final static boolean d = false; + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "mgdl") + public double mgdl; + + @Expose + @Column(name = "created_timestamp") + public long created_timestamp; + + @Expose + @Column(name = "state") + public long state; // bitfield + + @Expose + @Column(name = "source") + public String source; + + @Expose + @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public String uuid; + + + public GlucoseReadingRx glucoseReadingRx; + + // patches and saves + public Long saveit() { + fixUpTable(); + return save(); + } + + public void addState(long flag) { + state |= flag; + save(); + } + + public void removeState(long flag) { + state &= ~flag; + save(); + } + + public String toS() { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + return gson.toJson(this); + } + + private BloodTestMessage toMessageNative() { + return new BloodTestMessage.Builder() + .timestamp(timestamp) + .mgdl(mgdl) + .created_timestamp(created_timestamp) + .state(state) + .source(source) + .uuid(uuid) + .build(); + } + + public byte[] toMessage() { + final List btl = new ArrayList<>(); + btl.add(this); + return toMultiMessage(btl); + } + + + // static methods + private static final long CLOSEST_READING_MS = 30000; // 30 seconds + + public static BloodTest create(long timestamp_ms, double mgdl, String source) { + return create(timestamp_ms, mgdl, source, null); + } + + public static BloodTest create(long timestamp_ms, double mgdl, String source, String suggested_uuid) { + + if ((timestamp_ms == 0) || (mgdl == 0)) { + UserError.Log.e(TAG, "Either timestamp or mgdl is zero - cannot create reading"); + return null; + } + + if (timestamp_ms < 1487759433000L) { + UserError.Log.d(TAG, "Timestamp really too far in the past @ " + timestamp_ms); + return null; + } + + final long now = JoH.tsl(); + if (timestamp_ms > now) { + if ((timestamp_ms - now) > 600000) { + UserError.Log.wtf(TAG, "Timestamp is > 10 minutes in the future! Something is wrong: " + JoH.dateTimeText(timestamp_ms)); + return null; + } + timestamp_ms = now; // force to now if it showed up to 10 mins in the future + } + + final BloodTest match = getForPreciseTimestamp(timestamp_ms, CLOSEST_READING_MS); + if (match == null) { + final BloodTest bt = new BloodTest(); + bt.timestamp = timestamp_ms; + bt.mgdl = mgdl; + bt.uuid = suggested_uuid == null ? UUID.randomUUID().toString() : suggested_uuid; + bt.created_timestamp = JoH.tsl(); + bt.state = STATE_VALID; + bt.source = source; + bt.saveit(); + if (UploaderQueue.newEntry("insert", bt) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + + if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { + if ((JoH.msSince(bt.timestamp) < Constants.MINUTE_IN_MS * 5) && (JoH.msSince(bt.timestamp) > 0)) { + UserError.Log.d(TAG, "Blood test value recent enough to send to G5"); + //Ob1G5StateMachine.addCalibration((int) bt.mgdl, timestamp_ms); + NativeCalibrationPipe.addCalibration((int) bt.mgdl, timestamp_ms); + } + } + + return bt; + } else { + UserError.Log.d(TAG, "Not creating new reading as timestamp is too close"); + } + return null; + } + + public static BloodTest createFromCal(double bg, double timeoffset, String source) { + return createFromCal(bg, timeoffset, source, null); + } + + public static BloodTest createFromCal(double bg, double timeoffset, String source, String suggested_uuid) { + final String unit = Pref.getString("units", "mgdl"); + + if (unit.compareTo("mgdl") != 0) { + bg = bg * Constants.MMOLL_TO_MGDL; + } + + if ((bg < 40) || (bg > 400)) { + Log.wtf(TAG, "Invalid out of range bloodtest glucose mg/dl value of: " + bg); + JoH.static_toast_long("Bloodtest out of range: " + bg + " mg/dl"); + return null; + } + + return create((long) (new Date().getTime() - timeoffset), bg, source, suggested_uuid); + } + + public static void pushBloodTestSyncToWatch(BloodTest bt, boolean is_new) { + Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); + if (Pref.getBooleanDefaultFalse("wear_sync")) { + if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bt) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + + public static BloodTest last() { + final List btl = last(1); + if ((btl != null) && (btl.size() > 0)) { + return btl.get(0); + } else { + return null; + } + } + + public static List last(int num) { + try { + return new Select() + .from(BloodTest.class) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static List lastMatching(int num, String match) { + try { + return new Select() + .from(BloodTest.class) + .where("source like ?", match) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static BloodTest lastValid() { + final List btl = lastValid(1); + if ((btl != null) && (btl.size() > 0)) { + return btl.get(0); + } else { + return null; + } + } + + public static List lastValid(int num) { + try { + return new Select() + .from(BloodTest.class) + .where("state & ? != 0", BloodTest.STATE_VALID) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + + public static BloodTest byUUID(String uuid) { + if (uuid == null) return null; + try { + return new Select() + .from(BloodTest.class) + .where("uuid = ?", uuid) + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static BloodTest byid(long id) { + try { + return new Select() + .from(BloodTest.class) + .where("_ID = ?", id) + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static byte[] toMultiMessage(List btl) { + if (btl == null) return null; + final List BloodTestMessageList = new ArrayList<>(); + for (BloodTest bt : btl) { + BloodTestMessageList.add(bt.toMessageNative()); + } + return BloodTestMultiMessage.ADAPTER.encode(new BloodTestMultiMessage(BloodTestMessageList)); + } + + private static void processFromMessage(BloodTestMessage btm) { + if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) { + boolean is_new = false; + BloodTest bt = byUUID(btm.uuid); + if (bt == null) { + bt = getForPreciseTimestamp(Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS); + if (bt != null) { + UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bt.uuid + " vs " + btm.uuid + " skipping!"); + return; + } + bt = new BloodTest(); + is_new = true; + } else { + if (bt.state != Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE)) { + is_new = true; + } + } + bt.timestamp = Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP); + bt.mgdl = Wire.get(btm.mgdl, BloodTestMessage.DEFAULT_MGDL); + bt.created_timestamp = Wire.get(btm.created_timestamp, BloodTestMessage.DEFAULT_CREATED_TIMESTAMP); + bt.state = Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE); + bt.source = Wire.get(btm.source, BloodTestMessage.DEFAULT_SOURCE); + bt.uuid = btm.uuid; + bt.saveit(); // de-dupe by uuid + if (is_new) { // cannot handle updates yet + if (UploaderQueue.newEntry(is_new ? "insert" : "update", bt) != null) { + if (JoH.quietratelimit("start-sync-service", 5)) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + } else { + UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid"); + } + } + + public static void processFromMultiMessage(byte[] payload) { + try { + final BloodTestMultiMessage btmm = BloodTestMultiMessage.ADAPTER.decode(payload); + if ((btmm != null) && (btmm.bloodtest_message != null)) { + for (BloodTestMessage btm : btmm.bloodtest_message) { + processFromMessage(btm); + } + Home.staticRefreshBGCharts(); + } + } catch (IOException | NullPointerException | IllegalStateException e) { + UserError.Log.e(TAG, "exception processFromMessage: " + e); + } + } + + public static BloodTest fromJSON(String json) { + if ((json == null) || (json.length() == 0)) { + UserError.Log.d(TAG, "Empty json received in bloodtest fromJson"); + return null; + } + try { + UserError.Log.d(TAG, "Processing incoming json: " + json); + return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, BloodTest.class); + } catch (Exception e) { + UserError.Log.d(TAG, "Got exception parsing bloodtest json: " + e.toString()); + Home.toaststaticnext("Error on Bloodtest sync, probably decryption key mismatch"); + return null; + } + } + + public static BloodTest getForPreciseTimestamp(long timestamp, long precision) { + BloodTest bloodTest = new Select() + .from(BloodTest.class) + .where("timestamp <= ?", (timestamp + precision)) + .where("timestamp >= ?", (timestamp - precision)) + .orderBy("abs(timestamp - " + timestamp + ") asc") + .executeSingle(); + if ((bloodTest != null) && (Math.abs(bloodTest.timestamp - timestamp) < precision)) { + return bloodTest; + } + return null; + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + return new Select() + .from(BloodTest.class) + .where("state & ? != 0", BloodTest.STATE_VALID) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return new ArrayList<>(); + } + } + + synchronized static void opportunisticCalibration() { + if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { + final BloodTest bt = lastValid(); + if (bt == null) { + Log.d(TAG, "opportunistic: No blood tests"); + return; + } + if (JoH.msSince(bt.timestamp) > (Constants.HOUR_IN_MS * 8)) { + Log.d(TAG, "opportunistic: Blood test older than 8 hours ago"); + return; + } + + if ((bt.uuid == null) || (bt.uuid.length() < 8)) { + Log.d(TAG, "opportunisitic: invalid uuid"); + return; + } + + if ((bt.uuid != null) && (bt.uuid.length() > 1) && PersistentStore.getString(LAST_BT_AUTO_CALIB_UUID).equals(bt.uuid)) { + Log.d(TAG, "opportunistic: Already processed uuid: " + bt.uuid); + return; + } + + final Calibration calibration = Calibration.lastValid(); + if (calibration == null) { + Log.d(TAG, "opportunistic: No calibrations"); + // TODO do we try to initial calibrate using this? + return; + } + + if (JoH.msSince(calibration.timestamp) < Constants.HOUR_IN_MS) { + Log.d(TAG, "opportunistic: Last calibration less than 1 hour ago"); + return; + } + + if (bt.timestamp <= calibration.timestamp) { + Log.d(TAG, "opportunistic: Blood test isn't more recent than last calibration"); + return; + } + + // get closest bgreading - must be within dexcom period and locked to sensor + final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); + if (bgReading == null) { + Log.d(TAG, "opportunistic: No matching bg reading"); + return; + } + + if (bt.timestamp > highest_timestamp) { + Accuracy.create(bt, bgReading, "xDrip Original"); + final CalibrationAbstract plugin = PluggableCalibration.getCalibrationPluginFromPreferences(); + final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; + if (plugin != null) { + BgReading pluginBgReading = plugin.getBgReadingFromBgReading(bgReading, cd); + Accuracy.create(bt, pluginBgReading, plugin.getAlgorithmName()); + } + highest_timestamp = bt.timestamp; + } + + if (!CalibrationRequest.isSlopeFlatEnough(bgReading)) { + Log.d(TAG, "opportunistic: Slope is not flat enough at: " + JoH.dateTimeText(bgReading.timestamp)); + return; + } + + // TODO store evaluation failure for this record in cache for future optimization + + // TODO Check we have prior reading as well perhaps + JoH.clearCache(); + UserError.Log.ueh(TAG, "Opportunistic calibration for Blood Test at " + JoH.dateTimeText(bt.timestamp) + " of " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " matching sensor slope at: " + JoH.dateTimeText(bgReading.timestamp) + " from source " + bt.source); + final long time_since = JoH.msSince(bt.timestamp); + + + Log.d(TAG, "opportunistic: attempting auto calibration"); + PersistentStore.setString(LAST_BT_AUTO_CALIB_UUID, bt.uuid); + Home.startHomeWithExtra(xdrip.getAppContext(), + Home.BLUETOOTH_METER_CALIBRATION, + BgGraphBuilder.unitized_string_static(bt.mgdl), + Long.toString(time_since), + "auto"); + } + } + + public static String evaluateAccuracy(long period) { + + // CACHE?? + + final List bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds); + final List difference = new ArrayList<>(); + final List plugin_difference = new ArrayList<>(); + if ((bloodTests == null) || (bloodTests.size() == 0)) return null; + + final boolean show_plugin = true; + final CalibrationAbstract plugin = (show_plugin) ? PluggableCalibration.getCalibrationPluginFromPreferences() : null; + + + for (BloodTest bt : bloodTests) { + final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); + + if (bgReading != null) { + final Calibration calibration = bgReading.calibration; + if (calibration == null) { + Log.d(TAG, "Calibration for bgReading is null! @ " + JoH.dateTimeText(bgReading.timestamp)); + continue; + } + final double diff = Math.abs(bgReading.calculated_value - bt.mgdl); + difference.add(diff); + if (d) { + Log.d(TAG, "Evaluate Accuracy: difference: " + JoH.qs(diff)); + } + final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; + if ((plugin != null) && (cd != null)) { + final double plugin_diff = Math.abs(bt.mgdl - plugin.getGlucoseFromBgReading(bgReading, cd)); + plugin_difference.add(plugin_diff); + if (d) + Log.d(TAG, "Evaluate Plugin Accuracy: " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " difference: " + JoH.qs(plugin_diff) + "/" + JoH.qs(plugin_diff * Constants.MGDL_TO_MMOLL, 2) + " calibration: " + JoH.qs(cd.slope, 2) + " " + JoH.qs(cd.intercept, 2)); + } + } + } + + if (difference.size() == 0) return null; + double avg = DoubleMath.mean(difference); + Log.d(TAG, "Average accuracy: " + accuracyAsString(avg) + " (" + JoH.qs(avg, 5) + ")"); + + if (plugin_difference.size() > 0) { + double plugin_avg = DoubleMath.mean(plugin_difference); + Log.d(TAG, "Plugin Average accuracy: " + accuracyAsString(plugin_avg) + " (" + JoH.qs(plugin_avg, 5) + ")"); + return accuracyAsString(plugin_avg) + " / " + accuracyAsString(avg); + } + return accuracyAsString(avg); + } + + public static String accuracyAsString(double avg) { + final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl"); + // +- symbol + return "\u00B1" + (!domgdl ? JoH.qs(avg * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(avg, 1) + " mgdl"); + } + + public static List cleanup(int retention_days) { + return new Delete() + .from(BloodTest.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS)) + .execute(); + } + + // create the table ourselves without worrying about model versioning and downgrading + private static void fixUpTable() { + if (patched) return; + final String[] patchup = { + "CREATE TABLE BloodTest (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE BloodTest ADD COLUMN timestamp INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN created_timestamp INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN state INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN mgdl REAL;", + "ALTER TABLE BloodTest ADD COLUMN source TEXT;", + "ALTER TABLE BloodTest ADD COLUMN uuid TEXT;", + "CREATE UNIQUE INDEX index_Bloodtest_uuid on BloodTest(uuid);", + "CREATE UNIQUE INDEX index_Bloodtest_timestamp on BloodTest(timestamp);", + "CREATE INDEX index_Bloodtest_created_timestamp on BloodTest(created_timestamp);", + "CREATE INDEX index_Bloodtest_state on BloodTest(state);"}; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + // UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Bubble.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Bubble.java new file mode 100755 index 0000000..1887221 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Bubble.java @@ -0,0 +1,173 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.NFCReaderX; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.UtilityModels.BridgeResponse; +import com.eveningoutpost.dexdrip.UtilityModels.LibreUtils; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; + +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; + +import static com.eveningoutpost.dexdrip.xdrip.gs; + +/** + * + */ + +public class Bubble { + private static final String TAG = "Bubble";//?????"Bubble"; + + private static volatile byte[] s_full_data = null; + private static volatile int s_acumulatedSize = 0; + + public static boolean isBubble() { + final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + if (activeBluetoothDevice == null || activeBluetoothDevice.name == null) { + return false; + } + return activeBluetoothDevice.name.contentEquals("Bubble"); + } + + + public static BridgeResponse getBubbleResponse() { + final BridgeResponse reply = new BridgeResponse(); + ByteBuffer ackMessage = ByteBuffer.allocate(6); + ackMessage.put(0, (byte) 0x02); + ackMessage.put(1, (byte) 0x01); + ackMessage.put(2, (byte) 0x00); + ackMessage.put(3, (byte) 0x00); + ackMessage.put(4, (byte) 0x00); + ackMessage.put(5, (byte) 0x2B); + reply.add(ackMessage); + return reply; + } + + public static int lens = 344; + public static int BUBBLE_FOOTER = 8; + + static int errorCount = 0; + + + static byte[] patchUid = null; + static byte[] patchInfo = null; + + public static BridgeResponse decodeBubblePacket(byte[] buffer, int len) { + final BridgeResponse reply = new BridgeResponse(); + int first = 0xff & buffer[0]; + if (first == 0x80) { + PersistentStore.setString("Bubblebattery", Integer.toString(buffer[4])); + Pref.setInt("bridge_battery", buffer[4]); + String bubblefirmware = buffer[2] + "." + buffer[3]; + String bubbleHArdware = buffer[buffer.length-2] + "." + buffer[buffer.length-1]; + PersistentStore.setString("BubbleHArdware", bubbleHArdware); + PersistentStore.setString("BubbleFirmware", bubblefirmware); + ByteBuffer ackMessage = ByteBuffer.allocate(6); + ackMessage.put(0, (byte) 0x02); + ackMessage.put(1, (byte) 0x01); + ackMessage.put(2, (byte) 0x00); + ackMessage.put(3, (byte) 0x00); + ackMessage.put(4, (byte) 0x00); + ackMessage.put(5, (byte) 0x2B); + reply.add(ackMessage); + s_full_data = null; + return getBubbleResponse(); + } + if (first == 0xC0) { + patchUid = Arrays.copyOfRange(buffer, 2, 10); + String SensorSn = LibreUtils.decodeSerialNumberKey(patchUid); + PersistentStore.setString("LibreSN", SensorSn); + return reply; + } + if (first == 0xC1) { + double fv = JoH.tolerantParseDouble(PersistentStore.getString("BubbleFirmware")); + if (fv < 1.35) { + patchInfo = Arrays.copyOfRange(buffer, 3, 9); + } else { + if (buffer.length >= 11) { + patchInfo = Arrays.copyOfRange(buffer, 5, 11); + } + } + return reply; + } + if (first == 0x82) { + int expectedSize = lens + BUBBLE_FOOTER; + if (s_full_data == null) { + InitBuffer(expectedSize); + } + addData(buffer); + return reply; + + } + + if (first == 0xBF) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Log.e(TAG, "No sensor has been found"); + reply.setError_message(gs(R.string.no_sensor_found)); + s_full_data = null; + errorCount++; + if (errorCount <= 2) { + return getBubbleResponse(); + } + return reply; + } + + + return reply; + } + + + static void addData(byte[] buffer) { + System.arraycopy(buffer, 4, s_full_data, s_acumulatedSize, buffer.length-4); + s_acumulatedSize = s_acumulatedSize + buffer.length - 4; + AreWeDone(); + } + + + static void AreWeDone() { + if (s_acumulatedSize < lens) { + return; + } + long now = JoH.tsl(); + String SensorSn = PersistentStore.getString("LibreSN"); + + + byte[] data = Arrays.copyOfRange(s_full_data, 0, 344); + + boolean checksum_ok = NFCReaderX.HandleGoodReading(SensorSn, data, now, true, patchUid, patchInfo); + int expectedSize = lens + BUBBLE_FOOTER; + InitBuffer(expectedSize); + errorCount = 0; + Log.e(TAG, "We have all the data that we need " + s_acumulatedSize + " checksum_ok = " + checksum_ok + HexDump.dumpHexString(data)); + + } + + + static void InitBuffer(int expectedSize) { + s_full_data = new byte[expectedSize]; + s_acumulatedSize = 0; + } + + public static ArrayList initialize() { + Log.e(TAG, "initialize!"); + Pref.setInt("bridge_battery", 0); //force battery to no-value before first reading + return resetBubbleState(); + } + + private static ArrayList resetBubbleState() { + ArrayList ret = new ArrayList<>(); + + // Make Bubble send data every 5 minutes + ByteBuffer ackMessage = ByteBuffer.allocate(3); + ackMessage.put(0, (byte) 0x00); + ackMessage.put(1, (byte) 0x01); + ackMessage.put(2, (byte) 0x05); + ret.add(ackMessage); + return ret; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Calibration.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Calibration.java new file mode 100644 index 0000000..4bef985 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Calibration.java @@ -0,0 +1,1414 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; +import android.support.annotation.NonNull; +import android.widget.Toast; + +import com.activeandroid.ActiveAndroid; +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalRecord; +import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalSubrecord; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService; +import com.eveningoutpost.dexdrip.UtilityModels.BgSendQueue; +import com.eveningoutpost.dexdrip.UtilityModels.CalibrationSendQueue; +import com.eveningoutpost.dexdrip.UtilityModels.CollectionServiceStarter; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Notifications; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract; +import com.eveningoutpost.dexdrip.calibrations.NativeCalibrationPipe; +import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.google.gson.internal.bind.DateTypeAdapter; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static com.eveningoutpost.dexdrip.Models.BgReading.isDataSuitableForDoubleCalibration; +import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.newFingerStickData; + + +class DexParameters extends SlopeParameters { + DexParameters() { + LOW_SLOPE_1 = 0.75; + LOW_SLOPE_2 = 0.70; + HIGH_SLOPE_1 = 1.5; + HIGH_SLOPE_2 = 1.6; + DEFAULT_LOW_SLOPE_LOW = 0.75; + DEFAULT_LOW_SLOPE_HIGH = 0.70; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1.5; + DEFAULT_HIGH_SLOPE_LOW = 1.4; + } + +} + + +class DexOldSchoolParameters extends SlopeParameters { + /* + Previous defaults up until 20th March 2017 + */ + DexOldSchoolParameters() { + LOW_SLOPE_1 = 0.95; + LOW_SLOPE_2 = 0.85; + HIGH_SLOPE_1 = 1.3; + HIGH_SLOPE_2 = 1.4; + DEFAULT_LOW_SLOPE_LOW = 1.08; + DEFAULT_LOW_SLOPE_HIGH = 1.15; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1.3; + DEFAULT_HIGH_SLOPE_LOW = 1.2; + } + +} + +class DexParametersAdrian extends SlopeParameters { + + /* + * Other default vlaues and thresholds that can be only activated in settings, when in engineering mode. + * promoted to be the regular defaults 20th March 2017 + * */ + + DexParametersAdrian() { + LOW_SLOPE_1 = 0.75; + LOW_SLOPE_2 = 0.70; + HIGH_SLOPE_1 = 1.3; + HIGH_SLOPE_2 = 1.4; + DEFAULT_LOW_SLOPE_LOW = 0.75; + DEFAULT_LOW_SLOPE_HIGH = 0.70; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1.3; + DEFAULT_HIGH_SLOPE_LOW = 1.2; + } + +} + +class LiParameters extends SlopeParameters { + LiParameters() { + LOW_SLOPE_1 = 1; + LOW_SLOPE_2 = 1; + HIGH_SLOPE_1 = 1; + HIGH_SLOPE_2 = 1; + DEFAULT_LOW_SLOPE_LOW = 1; + DEFAULT_LOW_SLOPE_HIGH = 1; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1; + DEFAULT_HIGH_SLOPE_LOW = 1; + } +} + +/* Alternate Li Parameters which don't use a fixed slope */ +class LiParametersNonFixed extends SlopeParameters { + LiParametersNonFixed() { + LOW_SLOPE_1 = 0.55; + LOW_SLOPE_2 = 0.50; + HIGH_SLOPE_1 = 1.5; + HIGH_SLOPE_2 = 1.6; + DEFAULT_LOW_SLOPE_LOW = 0.55; + DEFAULT_LOW_SLOPE_HIGH = 0.50; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1.5; + DEFAULT_HIGH_SLOPE_LOW = 1.4; + } + +} + +class Li2AppParameters extends SlopeParameters { + Li2AppParameters() { + LOW_SLOPE_1 = 1; + LOW_SLOPE_2 = 1; + HIGH_SLOPE_1 = 1; + HIGH_SLOPE_2 = 1; + DEFAULT_LOW_SLOPE_LOW = 1; + DEFAULT_LOW_SLOPE_HIGH = 1; + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1; + DEFAULT_HIGH_SLOPE_LOW = 1; + } + + @Override + public double restrictIntercept(double intercept) { + return Math.min(Math.max(intercept, -40), 20); + } +} + +class TestParameters extends SlopeParameters { + TestParameters() { + LOW_SLOPE_1 = 0.85; //0.95 + LOW_SLOPE_2 = 0.80; //0.85 + HIGH_SLOPE_1 = 1.3; + HIGH_SLOPE_2 = 1.4; + DEFAULT_LOW_SLOPE_LOW = 0.9; //1.08 + DEFAULT_LOW_SLOPE_HIGH = 0.95; //1.15 + DEFAULT_SLOPE = 1; + DEFAULT_HIGH_SLOPE_HIGH = 1.3; + DEFAULT_HIGH_SLOPE_LOW = 1.2; + } +} + +/** + * Created by Emma Black on 10/29/14. + */ +@Table(name = "Calibration", id = BaseColumns._ID) +public class Calibration extends Model { + private final static String TAG = Calibration.class.getSimpleName(); + private final static double note_only_marker = 0.000001d; + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + @Expose + @Column(name = "sensor_age_at_time_of_estimation") + public double sensor_age_at_time_of_estimation; + + @Column(name = "sensor", index = true) + public Sensor sensor; + + @Expose + @Column(name = "bg") + public double bg; + + @Expose + @Column(name = "raw_value") + public double raw_value; +// +// @Expose +// @Column(name = "filtered_value") +// public double filtered_value; + + @Expose + @Column(name = "adjusted_raw_value") + public double adjusted_raw_value; + + @Expose + @Column(name = "sensor_confidence") + public double sensor_confidence; + + @Expose + @Column(name = "slope_confidence") + public double slope_confidence; + + @Expose + @Column(name = "raw_timestamp") + public long raw_timestamp; + + @Expose + @Column(name = "slope") + public double slope; + + @Expose + @Column(name = "intercept") + public double intercept; + + @Expose + @Column(name = "distance_from_estimate") + public double distance_from_estimate; + + @Expose + @Column(name = "estimate_raw_at_time_of_calibration") + public double estimate_raw_at_time_of_calibration; + + @Expose + @Column(name = "estimate_bg_at_time_of_calibration") + public double estimate_bg_at_time_of_calibration; + + @Expose + @Column(name = "uuid", index = true) + public String uuid; + + @Expose + @Column(name = "sensor_uuid", index = true) + public String sensor_uuid; + + @Expose + @Column(name = "possible_bad") + public Boolean possible_bad; + + @Expose + @Column(name = "check_in") + public boolean check_in; + + @Expose + @Column(name = "first_decay") + public double first_decay; + + @Expose + @Column(name = "second_decay") + public double second_decay; + + @Expose + @Column(name = "first_slope") + public double first_slope; + + @Expose + @Column(name = "second_slope") + public double second_slope; + + @Expose + @Column(name = "first_intercept") + public double first_intercept; + + @Expose + @Column(name = "second_intercept") + public double second_intercept; + + @Expose + @Column(name = "first_scale") + public double first_scale; + + @Expose + @Column(name = "second_scale") + public double second_scale; + + public static void initialCalibration(double bg1, double bg2, Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String unit = prefs.getString("units", "mgdl"); + if (unit.compareTo("mgdl") != 0) { + bg1 = bg1 * Constants.MMOLL_TO_MGDL; + bg2 = bg2 * Constants.MMOLL_TO_MGDL; + } + + JoH.clearCache(); + final Calibration higherCalibration = new Calibration(); + final Calibration lowerCalibration = new Calibration(); + final Sensor sensor = Sensor.currentSensor(); + final List bgReadings = BgReading.latest_by_size(3); + + // don't allow initial calibration if data would be stale (but still use data for native mode) + if ((bgReadings == null) || (bgReadings.size() != 3) || !isDataSuitableForDoubleCalibration() ){ + + if (Ob1G5CollectionService.usingNativeMode()) { + JoH.static_toast_long("Sending Blood Tests to Transmitter"); // TODO extract string + BloodTest.create(JoH.tsl() - (Constants.SECOND_IN_MS * 30), bg1, "Initial Calibration"); + BloodTest.create(JoH.tsl(), bg2, "Initial Calibration"); + + if (!Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { + // blood tests above don't automatically become part of calibration pipe if this setting is unset so do here + NativeCalibrationPipe.addCalibration((int) bg1, JoH.tsl() - (Constants.SECOND_IN_MS * 30)); + NativeCalibrationPipe.addCalibration((int) bg2, JoH.tsl()); + } + + } else { + UserError.Log.wtf(TAG, "Did not find 3 readings for initial calibration - aborting"); + JoH.static_toast_long("Not enough recent sensor data! - cancelling!"); + } + return; + } + + + BgReading bgReading1 = bgReadings.get(0); + BgReading bgReading2 = bgReadings.get(1); + + if (!SensorSanity.isRawValueSane(bgReading1.raw_data) || (!SensorSanity.isRawValueSane(bgReading2.raw_data))) { + final String msg = "Sensor raw data is outside sane range! Cannot calibrate: " + bgReading1.raw_data + " " + bgReading2.raw_data; + UserError.Log.wtf(TAG, msg); + JoH.static_toast_long(msg); + return; + } + + clear_all_existing_calibrations(); + + BgReading highBgReading; + BgReading lowBgReading; + double higher_bg = Math.max(bg1, bg2); + double lower_bg = Math.min(bg1, bg2); + + // TODO This should be reworked in the future as it doesn't really make sense + if (bgReading1.raw_data > bgReading2.raw_data) { + highBgReading = bgReading1; + lowBgReading = bgReading2; + } else { + highBgReading = bgReading2; + lowBgReading = bgReading1; + } + + higherCalibration.bg = higher_bg; + higherCalibration.slope = 1; + higherCalibration.intercept = higher_bg; + higherCalibration.sensor = sensor; + higherCalibration.estimate_raw_at_time_of_calibration = highBgReading.age_adjusted_raw_value; + higherCalibration.adjusted_raw_value = highBgReading.age_adjusted_raw_value; + higherCalibration.raw_value = highBgReading.raw_data; + higherCalibration.raw_timestamp = highBgReading.timestamp; + higherCalibration.save(); + + highBgReading.calculated_value = higher_bg; + highBgReading.calibration_flag = true; + highBgReading.calibration = higherCalibration; + highBgReading.save(); + higherCalibration.save(); + + lowerCalibration.bg = lower_bg; + lowerCalibration.slope = 1; + lowerCalibration.intercept = lower_bg; + lowerCalibration.sensor = sensor; + lowerCalibration.estimate_raw_at_time_of_calibration = lowBgReading.age_adjusted_raw_value; + lowerCalibration.adjusted_raw_value = lowBgReading.age_adjusted_raw_value; + lowerCalibration.raw_value = lowBgReading.raw_data; + lowerCalibration.raw_timestamp = lowBgReading.timestamp; + lowerCalibration.save(); + + lowBgReading.calculated_value = lower_bg; + lowBgReading.calibration_flag = true; + lowBgReading.calibration = lowerCalibration; + lowBgReading.save(); + lowerCalibration.save(); + + JoH.clearCache(); + highBgReading.find_new_curve(); + highBgReading.find_new_raw_curve(); + lowBgReading.find_new_curve(); + lowBgReading.find_new_raw_curve(); + + JoH.clearCache(); + + + NativeCalibrationPipe.addCalibration((int) bg1, JoH.tsl() - (Constants.SECOND_IN_MS * 30)); + NativeCalibrationPipe.addCalibration((int) bg2, JoH.tsl()); + + + final List calibrations = new ArrayList(); + calibrations.add(lowerCalibration); + calibrations.add(higherCalibration); + + for (Calibration calibration : calibrations) { + calibration.timestamp = new Date().getTime(); + calibration.sensor_uuid = sensor.uuid; + calibration.slope_confidence = .5; + calibration.distance_from_estimate = 0; + calibration.check_in = false; + calibration.sensor_confidence = ((-0.0018 * calibration.bg * calibration.bg) + (0.6657 * calibration.bg) + 36.7505) / 100; + + calibration.sensor_age_at_time_of_estimation = calibration.timestamp - sensor.started_at; + calibration.uuid = UUID.randomUUID().toString(); + calibration.save(); + JoH.clearCache(); + calculate_w_l_s(); + newFingerStickData(); + CalibrationSendQueue.addToQueue(calibration, context); + } + JoH.clearCache(); + if (!Ob1G5CollectionService.usingNativeMode()) { + adjustRecentBgReadings(5); + } + CalibrationRequest.createOffset(lowerCalibration.bg, 35); + Notifications.staticUpdateNotification(); + } + + //Create Calibration Checkin Dexcom Bluetooth Share + public static void create(CalRecord[] calRecords, long addativeOffset, Context context) { + create(calRecords, context, false, addativeOffset); + } + + public static void create(CalRecord[] calRecords, Context context) { + create(calRecords, context, false, 0); + } + + // Bluetooth Share + public static void create(CalRecord[] calRecords, Context context, boolean override, long addativeOffset) { + //TODO: Change calibration.last and other queries to order calibrations by timestamp rather than ID + Log.i("CALIBRATION-CHECK-IN: ", "Creating Calibration Record"); + Sensor sensor = Sensor.currentSensor(); + CalRecord firstCalRecord = calRecords[0]; + CalRecord secondCalRecord = calRecords[0]; +// CalRecord secondCalRecord = calRecords[calRecords.length - 1]; + //TODO: Figgure out how the ratio between the two is determined + double calSlope = ((secondCalRecord.getScale() / secondCalRecord.getSlope()) + (3 * firstCalRecord.getScale() / firstCalRecord.getSlope())) * 250; + + double calIntercept = (((secondCalRecord.getScale() * secondCalRecord.getIntercept()) / secondCalRecord.getSlope()) + ((3 * firstCalRecord.getScale() * firstCalRecord.getIntercept()) / firstCalRecord.getSlope())) / -4; + if (sensor != null) { + for (int i = 0; i < firstCalRecord.getCalSubrecords().length - 1; i++) { + if (((firstCalRecord.getCalSubrecords()[i] != null && Calibration.is_new(firstCalRecord.getCalSubrecords()[i], addativeOffset))) || (i == 0 && override)) { + CalSubrecord calSubrecord = firstCalRecord.getCalSubrecords()[i]; + + Calibration calibration = new Calibration(); + calibration.bg = calSubrecord.getCalBGL(); + calibration.timestamp = calSubrecord.getDateEntered().getTime() + addativeOffset; + calibration.raw_timestamp = calibration.timestamp; + if (calibration.timestamp > new Date().getTime()) { + Log.d(TAG, "ERROR - Calibration timestamp is from the future, wont save!"); + return; + } + calibration.raw_value = calSubrecord.getCalRaw() / 1000; + calibration.slope = calSlope; + calibration.intercept = calIntercept; + + calibration.sensor_confidence = ((-0.0018 * calibration.bg * calibration.bg) + (0.6657 * calibration.bg) + 36.7505) / 100; + if (calibration.sensor_confidence <= 0) { + calibration.sensor_confidence = 0; + } + calibration.slope_confidence = 0.8; //TODO: query backwards to find this value near the timestamp + calibration.estimate_raw_at_time_of_calibration = calSubrecord.getCalRaw() / 1000; + calibration.sensor = sensor; + calibration.sensor_age_at_time_of_estimation = calibration.timestamp - sensor.started_at; + calibration.uuid = UUID.randomUUID().toString(); + calibration.sensor_uuid = sensor.uuid; + calibration.check_in = true; + + calibration.first_decay = firstCalRecord.getDecay(); + calibration.second_decay = secondCalRecord.getDecay(); + calibration.first_slope = firstCalRecord.getSlope(); + calibration.second_slope = secondCalRecord.getSlope(); + calibration.first_scale = firstCalRecord.getScale(); + calibration.second_scale = secondCalRecord.getScale(); + calibration.first_intercept = firstCalRecord.getIntercept(); + calibration.second_intercept = secondCalRecord.getIntercept(); + + calibration.save(); + CalibrationSendQueue.addToQueue(calibration, context); + Calibration.requestCalibrationIfRangeTooNarrow(); + newFingerStickData(); + } + } + if (firstCalRecord.getCalSubrecords()[0] != null && firstCalRecord.getCalSubrecords()[2] == null) { + if (Calibration.latest(2).size() == 1) { + Calibration.create(calRecords, context, true, 0); + } + } + Notifications.start(); + } + } + + public static boolean is_new(CalSubrecord calSubrecord, long addativeOffset) { + Sensor sensor = Sensor.currentSensor(); + Calibration calibration = new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp <= ?", calSubrecord.getDateEntered().getTime() + addativeOffset + (1000 * 60 * 2)) + .orderBy("timestamp desc") + .executeSingle(); + if (calibration != null && Math.abs(calibration.timestamp - (calSubrecord.getDateEntered().getTime() + addativeOffset)) < (4 * 60 * 1000)) { + Log.d("CAL CHECK IN ", "Already have that calibration!"); + return false; + } else { + Log.d("CAL CHECK IN ", "Looks like a new calibration!"); + return true; + } + } + + public static Calibration getForTimestamp(double timestamp) { + Sensor sensor = Sensor.currentSensor(); + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("timestamp < ?", timestamp) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static Calibration getByTimestamp(double timestamp) {//KS + Sensor sensor = Sensor.currentSensor(); + if(sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp = ?", timestamp) + .executeSingle(); + } + + public static Double getConvertedBg(double bg) { + final String unit = Pref.getString("units", "mgdl"); + if (unit.compareTo("mgdl") != 0) { + bg = bg * Constants.MMOLL_TO_MGDL; + } + if ((bg < 40) || (bg > 400)) { + return null; + } + return bg; + } + + // without timeoffset + public static Calibration create(double bg, Context context) { + return create(bg, 0, context); + + } + + public static Calibration create(double bg, long timeoffset, Context context) { + return create(bg, timeoffset, context, false, 0); + } + + // regular calibration + public static Calibration create(double bg, long timeoffset, Context context, boolean note_only, long estimatedInterstitialLagSeconds) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String unit = prefs.getString("units", "mgdl"); + final boolean adjustPast = prefs.getBoolean("rewrite_history", true); + + final Double result = getConvertedBg(bg); + if (result == null) { + Log.wtf(TAG, "Invalid out of range calibration glucose mg/dl value of: " + bg); + JoH.static_toast_long("Calibration out of range: " + bg + " mg/dl"); + return null; + } + + bg = result; // unbox result + + if (!note_only) CalibrationRequest.clearAll(); + final Calibration calibration = new Calibration(); + Sensor sensor = Sensor.currentSensor(); + + boolean is_follower = prefs.getString("dex_collection_method", "").equals("Follower"); + if ((sensor == null) + && (is_follower)) { + Sensor.create(Math.round(JoH.ts())); // no sensor? no problem, create virtual one for follower + sensor = Sensor.currentSensor(); + } + + if (sensor != null) { + BgReading bgReading = null; + if (timeoffset == 0) { + bgReading = BgReading.last(is_follower); + } else { + // get closest bg reading we can find with a cut off at 15 minutes max time + bgReading = BgReading.getForPreciseTimestamp(new Date().getTime() - ((timeoffset - estimatedInterstitialLagSeconds) * 1000 ), (15 * 60 * 1000)); + } + if (bgReading != null) { + if (SensorSanity.isRawValueSane(bgReading.raw_data, DexCollectionType.getDexCollectionType(), true)) { + calibration.sensor = sensor; + calibration.bg = bg; + calibration.check_in = false; + calibration.timestamp = new Date().getTime() - (timeoffset * 1000); // potential historical bg readings + calibration.raw_value = bgReading.raw_data; + calibration.adjusted_raw_value = bgReading.age_adjusted_raw_value; + calibration.sensor_uuid = sensor.uuid; + calibration.slope_confidence = Math.min(Math.max(((4 - Math.abs((bgReading.calculated_value_slope) * 60000)) / 4), 0), 1); + + double estimated_raw_bg = BgReading.estimated_raw_bg(new Date().getTime()); + calibration.raw_timestamp = bgReading.timestamp; + if (Math.abs(estimated_raw_bg - bgReading.age_adjusted_raw_value) > 20) { + calibration.estimate_raw_at_time_of_calibration = bgReading.age_adjusted_raw_value; + } else { + calibration.estimate_raw_at_time_of_calibration = estimated_raw_bg; + } + calibration.distance_from_estimate = Math.abs(calibration.bg - bgReading.calculated_value); + if (!note_only) { + calibration.sensor_confidence = Math.max(((-0.0018 * bg * bg) + (0.6657 * bg) + 36.7505) / 100, 0); + } else { + calibration.sensor_confidence = 0; // exclude from calibrations but show on graph + calibration.slope_confidence = note_only_marker; // this is a bit ugly + calibration.slope = 0; + calibration.intercept = 0; + } + calibration.sensor_age_at_time_of_estimation = calibration.timestamp - sensor.started_at; + calibration.uuid = UUID.randomUUID().toString(); + + if (!SensorSanity.isRawValueSane(calibration.estimate_raw_at_time_of_calibration, true)) { + JoH.static_toast_long("Estimated raw value out of range - cannot calibrate"); + return null; + } + + calibration.save(); + + if (!note_only) { + bgReading.calibration = calibration; + bgReading.calibration_flag = true; + bgReading.save(); + } + + if ((!is_follower) && (!note_only)) { + BgSendQueue.handleNewBgReading(bgReading, "update", context); + // TODO probably should add a more fine grained prefs option in future + calculate_w_l_s(prefs.getBoolean("infrequent_calibration", false)); + CalibrationSendQueue.addToQueue(calibration, context); + BgReading.pushBgReadingSyncToWatch(bgReading, false); + if (!Ob1G5CollectionService.usingNativeMode()) { + adjustRecentBgReadings(adjustPast ? 30 : 2); + } + Notifications.start(); + Calibration.requestCalibrationIfRangeTooNarrow(); + newFingerStickData(); + } else { + Log.d(TAG, "Follower mode or note so not processing calibration deeply"); + } + } else { + final String msg = "Sensor data fails sanity test - Cannot Calibrate! raw:" + bgReading.raw_data; + UserError.Log.e(TAG, msg); + JoH.static_toast_long(msg); + } + } else { + // we couldn't get a reading close enough to the calibration timestamp + if (!is_follower) { + JoH.static_toast(context, "No close enough reading for Calib (15 min)", Toast.LENGTH_LONG); + } + } + } else { + Log.d("CALIBRATION", "No sensor, cant save!"); + } + return Calibration.last(); + } + + public static List allForSensorInLastFiveDays() { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("timestamp > ?", (new Date().getTime() - (60000 * 60 * 24 * 5))) + .orderBy("timestamp desc") + .execute(); + } + + private synchronized static void calculate_w_l_s() { + calculate_w_l_s(false); + } + + private synchronized static void calculate_w_l_s(boolean extended) { + if (Sensor.isActive()) { + double l = 0; + double m = 0; + double n = 0; + double p = 0; + double q = 0; + double w; + + final SlopeParameters sParams = getSlopeParameters(); + ActiveAndroid.clearCache(); + List calibrations = allForSensorInLastFourDays(); //5 days was a bit much, dropped this to 4 + + if (calibrations == null) { + Log.e(TAG, "Somehow ended up with null calibration list!"); + Home.toaststatic("Somehow ended up with null calibration list!"); + return; + } + + // less than 5 calibrations in last 4 days? cast the net wider if in extended mode + final int ccount = calibrations.size(); + if ((ccount < 5) && extended) { + ActiveAndroid.clearCache(); + calibrations = allForSensorLimited(5); + if (calibrations.size() > ccount) { + Home.toaststaticnext("Calibrated using data beyond last 4 days"); + } + } + ActiveAndroid.clearCache(); + if (calibrations.size() <= 1) { + final Calibration calibration = Calibration.last(); + ActiveAndroid.clearCache(); + calibration.slope = 1; + calibration.intercept = sParams.restrictIntercept(calibration.bg - (calibration.raw_value * calibration.slope)); + calibration.save(); + CalibrationRequest.createOffset(calibration.bg, 25); + newFingerStickData(); + } else { + for (Calibration calibration : calibrations) { + w = calibration.calculateWeight(); + l += (w); + m += (w * calibration.estimate_raw_at_time_of_calibration); + n += (w * calibration.estimate_raw_at_time_of_calibration * calibration.estimate_raw_at_time_of_calibration); + p += (w * calibration.bg); + q += (w * calibration.estimate_raw_at_time_of_calibration * calibration.bg); + } + + final Calibration last_calibration = Calibration.last(); + if (last_calibration != null) { + ActiveAndroid.clearCache(); + w = (last_calibration.calculateWeight() * (calibrations.size() * 0.14)); + l += (w); + m += (w * last_calibration.estimate_raw_at_time_of_calibration); + n += (w * last_calibration.estimate_raw_at_time_of_calibration * last_calibration.estimate_raw_at_time_of_calibration); + p += (w * last_calibration.bg); + q += (w * last_calibration.estimate_raw_at_time_of_calibration * last_calibration.bg); + } + + double d = (l * n) - (m * m); + final Calibration calibration = Calibration.last(); + ActiveAndroid.clearCache(); + calibration.intercept = sParams.restrictIntercept(((n * p) - (m * q)) / d); + calibration.slope = ((l * q) - (m * p)) / d; + Log.d(TAG, "Calibration slope debug: slope:" + calibration.slope + " q:" + q + " m:" + m + " p:" + p + " d:" + d); + if ((calibrations.size() == 2 && calibration.slope < sParams.getLowSlope1()) || (calibration.slope < sParams.getLowSlope2())) { // I have not seen a case where a value below 7.5 proved to be accurate but we should keep an eye on this + Log.d(TAG, "calibration.slope 1 : " + calibration.slope); + calibration.slope = calibration.slopeOOBHandler(0); + Log.d(TAG, "calibration.slope 2 : " + calibration.slope); + if (calibrations.size() > 2) { + calibration.possible_bad = true; + } + calibration.intercept = sParams.restrictIntercept(calibration.bg - (calibration.estimate_raw_at_time_of_calibration * calibration.slope)); + CalibrationRequest.createOffset(calibration.bg, 25); + } + if ((calibrations.size() == 2 && calibration.slope > sParams.getHighSlope1()) || (calibration.slope > sParams.getHighSlope2())) { + Log.d(TAG, "calibration.slope 3 : " + calibration.slope); + calibration.slope = calibration.slopeOOBHandler(1); + Log.d(TAG, "calibration.slope 4 : " + calibration.slope); + if (calibrations.size() > 2) { + calibration.possible_bad = true; + } + calibration.intercept = sParams.restrictIntercept(calibration.bg - (calibration.estimate_raw_at_time_of_calibration * calibration.slope)); + CalibrationRequest.createOffset(calibration.bg, 25); + } + Log.d(TAG, "Calculated Calibration Slope: " + calibration.slope); + Log.d(TAG, "Calculated Calibration intercept: " + calibration.intercept); + + // sanity check result + if (Double.isInfinite(calibration.slope) + ||(Double.isNaN(calibration.slope)) + ||(Double.isInfinite(calibration.intercept)) + ||(Double.isNaN(calibration.intercept))) { + calibration.sensor_confidence = 0; + calibration.slope_confidence = 0; + Home.toaststaticnext("Got invalid impossible slope calibration!"); + calibration.save(); // Save nulled record, lastValid should protect from bad calibrations + newFingerStickData(); + } + + if ((calibration.slope == 0) && (calibration.intercept == 0)) { + calibration.sensor_confidence = 0; + calibration.slope_confidence = 0; + Home.toaststaticnext("Got invalid zero slope calibration!"); + calibration.save(); // Save nulled record, lastValid should protect from bad calibrations + newFingerStickData(); + } else if (calibration.intercept > CalibrationAbstract.getHighestSaneIntercept()) { + /* + calibration.sensor_confidence = 0; + calibration.slope_confidence = 0; + final String msg = "Got invalid non-sane intercept calibration! "; + Home.toaststaticnext(msg); + UserError.Log.wtf(TAG, msg + calibration.toS()); + */ + // Just log the error but store the calibration so we can use it in a plugin situation. lastValid() will filter it from calculations. + UserError.Log.e(TAG, "Got invalid intercept value in xDrip classic algorithm: " + calibration.intercept); + calibration.save(); // save record, lastValid should protect from bad calibrations + newFingerStickData(); + + } else { + calibration.save(); + newFingerStickData(); + } + } + } else { + Log.d(TAG, "NO Current active sensor found!!"); + } + } + + @NonNull + private static SlopeParameters getSlopeParameters() { + + if (CollectionServiceStarter.isLibre2App((Context)null)) { + return new Li2AppParameters(); + } + + if (CollectionServiceStarter.isLimitter()) { + if (Pref.getBooleanDefaultFalse("use_non_fixed_li_parameters")) { + return new LiParametersNonFixed(); + } else { + return new LiParameters(); + } + } + // open question about parameters used with LibreAlarm + + if (Pref.getBooleanDefaultFalse("engineering_mode") && Pref.getBooleanDefaultFalse("old_school_calibration_mode")) { + JoH.static_toast_long("Using old pre-2017 calibration mode!"); + return new DexOldSchoolParameters(); + } + + return new DexParameters(); + } + + // here be dragons.. at time of writing estimate_bg_at_time_of_calibration is never written to and the possible_bad logic below looks backwards but + // will never fire because the bg_at_time_of_calibration is not set. + private double slopeOOBHandler(int status) { + + final SlopeParameters sParams = getSlopeParameters(); + + // If the last slope was reasonable and reasonably close, use that, otherwise use a slope that may be a little steep, but its best to play it safe when uncertain + final List calibrations = Calibration.latest(3); + final Calibration thisCalibration = calibrations.get(0); + if (status == 0) { + if (calibrations.size() == 3) { + if ((Math.abs(thisCalibration.bg - thisCalibration.estimate_bg_at_time_of_calibration) < 30) + && (calibrations.get(1).slope != 0) + && (calibrations.get(1).possible_bad != null && calibrations.get(1).possible_bad == true)) { + return calibrations.get(1).slope; + } else { + return Math.max(((-0.048) * (thisCalibration.sensor_age_at_time_of_estimation / (60000 * 60 * 24))) + 1.1, sParams.getDefaultLowSlopeLow()); + } + } else if (calibrations.size() == 2) { + return Math.max(((-0.048) * (thisCalibration.sensor_age_at_time_of_estimation / (60000 * 60 * 24))) + 1.1, sParams.getDefaultLowSlopeHigh()); + } + return sParams.getDefaultSlope(); + } else { + if (calibrations.size() == 3) { + if ((Math.abs(thisCalibration.bg - thisCalibration.estimate_bg_at_time_of_calibration) < 30) + && (calibrations.get(1).slope != 0) + && (calibrations.get(1).possible_bad != null && calibrations.get(1).possible_bad == true)) { + return calibrations.get(1).slope; + } else { + return sParams.getDefaultHighSlopeHigh(); + } + } else if (calibrations.size() == 2) { + return sParams.getDefaulHighSlopeLow(); + } + } + return sParams.getDefaultSlope(); + } + + private static List calibrations_for_sensor(Sensor sensor) { + return new Select() + .from(Calibration.class) + .where("Sensor = ?", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .orderBy("timestamp desc") + .execute(); + } + + private double calculateWeight() { + double firstTimeStarted = Calibration.first().sensor_age_at_time_of_estimation; + double lastTimeStarted = Calibration.last().sensor_age_at_time_of_estimation; + double time_percentage = Math.min(((sensor_age_at_time_of_estimation - firstTimeStarted) / (lastTimeStarted - firstTimeStarted)) / (.85), 1); + time_percentage = (time_percentage + .01); + Log.i(TAG, "CALIBRATIONS TIME PERCENTAGE WEIGHT: " + time_percentage); + return Math.max((((((slope_confidence + sensor_confidence) * (time_percentage))) / 2) * 100), 1); + } + + + public static void adjustRecentBgReadings(int adjustCount) { + //TODO: add some handling around calibration overrides as they come out looking a bit funky + final List calibrations = Calibration.latest(3); + if (calibrations == null) { + Log.wtf(TAG, "Calibrations is null in adjustRecentBgReadings"); + return; + } + + final List bgReadings = BgReading.latestUnCalculated(adjustCount); + if (bgReadings == null) { + Log.wtf(TAG, "bgReadings is null in adjustRecentBgReadings"); + return; + } + + // ongoing calibration + if (calibrations.size() >= 3) { + final int denom = bgReadings.size(); + //Calibration latestCalibration = calibrations.get(0); + try { + final Calibration latestCalibration = Calibration.lastValid(); + int i = 0; + for (BgReading bgReading : bgReadings) { + if (bgReading.calibration != null) { + final double oldYValue = bgReading.calculated_value; + final double newYvalue = (bgReading.age_adjusted_raw_value * latestCalibration.slope) + latestCalibration.intercept; + final double new_calculated_value = ((newYvalue * (denom - i)) + (oldYValue * (i))) / denom; + // if filtered == raw then rewrite them both because this would not happen if filtered data was from real source + if (bgReading.filtered_calculated_value == bgReading.calculated_value) { + bgReading.filtered_calculated_value = new_calculated_value; + } + bgReading.calculated_value = new_calculated_value; + + bgReading.save(); + BgReading.pushBgReadingSyncToWatch(bgReading, false); + i += 1; + } else { + Log.d(TAG, "History Rewrite: Ignoring BgReading without calibration from: " + JoH.dateTimeText(bgReading.timestamp)); + } + } + } catch (NullPointerException e) { + Log.wtf(TAG, "Null pointer in AdjustRecentReadings >=3: " + e); + } + // initial calibration + } else if (calibrations.size() == 2) { + //Calibration latestCalibration = calibrations.get(0); + try { + final Calibration latestCalibration = Calibration.lastValid(); + for (BgReading bgReading : bgReadings) { + final double newYvalue = (bgReading.age_adjusted_raw_value * latestCalibration.slope) + latestCalibration.intercept; + if (bgReading.filtered_calculated_value == bgReading.calculated_value) { + bgReading.filtered_calculated_value = newYvalue; + } + bgReading.calculated_value = newYvalue; + BgReading.updateCalculatedValueToWithinMinMax(bgReading); + bgReading.save(); + BgReading.pushBgReadingSyncToWatch(bgReading, false); + } + } catch (NullPointerException e) { + Log.wtf(TAG, "Null pointer in AdjustRecentReadings ==2: " + e); + } + } + + try { + // TODO this method call is probably only needed when we are called for initial calibration, it should probably be moved + bgReadings.get(0).find_new_raw_curve(); + bgReadings.get(0).find_new_curve(); + BgReading.pushBgReadingSyncToWatch(bgReadings.get(0), false); + } catch (NullPointerException e) { + Log.wtf(TAG, "Got null pointer exception in adjustRecentBgReadings"); + } + } + + public void rawValueOverride(double rawValue, Context context) { + estimate_raw_at_time_of_calibration = rawValue; + save(); + calculate_w_l_s(); + CalibrationSendQueue.addToQueue(this, context); + } + + public static void requestCalibrationIfRangeTooNarrow() { + double max = Calibration.max_recent(); + double min = Calibration.min_recent(); + if ((max - min) < 55) { + double avg = ((min + max) / 2); + double dist = max - avg; + CalibrationRequest.createOffset(avg, dist + 20); + } + } + + public static void clear_all_existing_calibrations() { + CalibrationRequest.clearAll(); + List pastCalibrations = Calibration.allForSensor(); + if (pastCalibrations != null) { + for (Calibration calibration : pastCalibrations) { + calibration.slope_confidence = 0; + calibration.sensor_confidence = 0; + calibration.save(); + newFingerStickData(); + } + } + } + + public static long msSinceLastCalibration() { + final Calibration calibration = lastValid(); + if (calibration == null) return 86400000000L; + return JoH.msSince(calibration.timestamp); + } + + public static void clearLastCalibration() { + CalibrationRequest.clearAll(); + Log.d(TAG, "Trying to clear last calibration"); + Calibration calibration = Calibration.last(); + if (calibration != null) { + calibration.invalidate(); + CalibrationSendQueue.addToQueue(calibration, xdrip.getAppContext()); + newFingerStickData(); + } + } + + public static void clearCalibrationByUUID(String uuid) { + final Calibration calibration = Calibration.byuuid(uuid); + if (calibration != null) { + CalibrationRequest.clearAll(); + Log.d(TAG, "Trying to clear last calibration: " + uuid); + calibration.invalidate(); + CalibrationSendQueue.addToQueue(calibration, xdrip.getAppContext()); + newFingerStickData(); + } else { + Log.d(TAG,"Could not find calibration to clear: "+uuid); + } + } + + + + public String toS() { + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + return gson.toJson(this); + } + + public static Calibration byid(long id) { + return new Select() + .from(Calibration.class) + .where("_ID = ?", id) + .executeSingle(); + } + + public static Calibration byuuid(String uuid) { + if (uuid == null) return null; + return new Select() + .from(Calibration.class) + .where("uuid = ?", uuid) + .orderBy("_ID desc") + .executeSingle(); + } + + public static void clear_byuuid(String uuid, boolean from_interactive) { + if (uuid == null) return; + Calibration calibration = byuuid(uuid); + if (calibration != null) { + calibration.invalidate(); + CalibrationSendQueue.addToQueue(calibration, xdrip.getAppContext()); + newFingerStickData(); + if (from_interactive) { + GcmActivity.clearLastCalibration(uuid); + } + } + } + + public static void upsertFromMaster(Calibration jsonCalibration) { + + if (jsonCalibration == null) { + Log.wtf(TAG,"Got null calibration from json"); + return; + } + try { + Sensor sensor = Sensor.getByUuid(jsonCalibration.sensor_uuid); + if (sensor == null) { + Log.e(TAG, "No sensor found, ignoring cailbration " + jsonCalibration.sensor_uuid); + return; + } + Calibration existingCalibration = byuuid(jsonCalibration.uuid); + if (existingCalibration == null) { + Log.d(TAG, "saving new calibration record. sensor uuid =" + jsonCalibration.sensor_uuid + " calibration uuid = " + jsonCalibration.uuid); + jsonCalibration.sensor = sensor; + jsonCalibration.save(); + } else { + Log.d(TAG, "updating existing calibration record: " + jsonCalibration.uuid); + existingCalibration.sensor = sensor; + existingCalibration.timestamp = jsonCalibration.timestamp; + existingCalibration.sensor_age_at_time_of_estimation = jsonCalibration.sensor_age_at_time_of_estimation; + existingCalibration.bg = jsonCalibration.bg; + existingCalibration.raw_value = jsonCalibration.raw_value; + existingCalibration.adjusted_raw_value = jsonCalibration.adjusted_raw_value; + existingCalibration.sensor_confidence = jsonCalibration.sensor_confidence; + existingCalibration.slope_confidence = jsonCalibration.slope_confidence; + existingCalibration.raw_timestamp = jsonCalibration.raw_timestamp; + existingCalibration.slope = jsonCalibration.slope; + existingCalibration.intercept = jsonCalibration.intercept; + existingCalibration.distance_from_estimate = jsonCalibration.distance_from_estimate; + existingCalibration.estimate_raw_at_time_of_calibration = jsonCalibration.estimate_raw_at_time_of_calibration; + existingCalibration.estimate_bg_at_time_of_calibration = jsonCalibration.estimate_bg_at_time_of_calibration; + existingCalibration.uuid = jsonCalibration.uuid; + existingCalibration.sensor_uuid = jsonCalibration.sensor_uuid; + existingCalibration.possible_bad = jsonCalibration.possible_bad; + existingCalibration.check_in = jsonCalibration.check_in; + existingCalibration.first_decay = jsonCalibration.first_decay; + existingCalibration.second_decay = jsonCalibration.second_decay; + existingCalibration.first_slope = jsonCalibration.first_slope; + existingCalibration.second_slope = jsonCalibration.second_slope; + existingCalibration.first_intercept = jsonCalibration.first_intercept; + existingCalibration.second_intercept = jsonCalibration.second_intercept; + existingCalibration.first_scale = jsonCalibration.first_scale; + existingCalibration.second_scale = jsonCalibration.second_scale; + + existingCalibration.save(); + } + } catch (Exception e) { + Log.e(TAG, "Could not save Calibration: " + e.toString()); + } + } + + + //COMMON SCOPES! + public static Calibration last() { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static Calibration lastValid() { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("slope != 0") + .where("intercept <= ?", CalibrationAbstract.getHighestSaneIntercept()) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static Calibration first() { + Sensor sensor = Sensor.currentSensor(); + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .orderBy("timestamp asc") + .executeSingle(); + } + + public static double max_recent() { + Sensor sensor = Sensor.currentSensor(); + Calibration calibration = new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("timestamp > ?", (new Date().getTime() - (60000 * 60 * 24 * 4))) + .orderBy("bg desc") + .executeSingle(); + if (calibration != null) { + return calibration.bg; + } else { + return 120; + } + } + + public static double min_recent() { + Sensor sensor = Sensor.currentSensor(); + Calibration calibration = new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("timestamp > ?", (new Date().getTime() - (60000 * 60 * 24 * 4))) + .orderBy("bg asc") + .executeSingle(); + if (calibration != null) { + return calibration.bg; + } else { + return 100; + } + } + + public static List latest(int number) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + // TODO calls to this method are used for UI features as to whether calibration is needed + // TODO this might need to updated to ignore invalid intercepts depending on plugin configuration etc + public static List latestValid(int number) { + return latestValid(number, JoH.tsl() + Constants.HOUR_IN_MS); + } + + public static List latestValid(int number, long until) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + // we don't filter invalid intercepts here as they will be filtered in the plugin itself + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("slope != 0") + .where("timestamp <= ?", until) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, (long)JoH.ts()); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + return new Select() + .from(Calibration.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .where("(slope != 0 or slope_confidence = ?)", note_only_marker) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List latestForGraphSensor(int number, long startTime, long endTime) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .where("(slope != 0 or slope_confidence = ?)", note_only_marker) + .orderBy("timestamp desc") + .limit(number) + .execute(); + } + + public static List allForSensor() { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .orderBy("timestamp desc") + .execute(); + } + + public static List allForSensorInLastFourDays() { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .where("timestamp > ?", (new Date().getTime() - (60000 * 60 * 24 * 4))) + .orderBy("timestamp desc") + .execute(); + } + + public static List allForSensorLimited(int limit) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + return null; + } + return new Select() + .from(Calibration.class) + .where("Sensor = ? ", sensor.getId()) + .where("slope_confidence != 0") + .where("sensor_confidence != 0") + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + public static List getCalibrationsForSensor(Sensor sensor, int limit) { + return new Select() + .from(Calibration.class) + .where("sensor_uuid = ? ", sensor.uuid) + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + public static List futureCalibrations() { + double timestamp = new Date().getTime(); + return new Select() + .from(Calibration.class) + .where("timestamp > " + timestamp) + .orderBy("timestamp desc") + .execute(); + } + + public boolean isNote() { + Calibration calibration = this; + if ((calibration.slope == 0) + && (calibration.slope_confidence == note_only_marker) + && (calibration.sensor_confidence == 0) + && (calibration.intercept == 0)) { + return true; + } else { + return false; + } + } + + public boolean isValid() { + Calibration calibration = this; + if ((calibration.slope_confidence != 0) + && (calibration.sensor_confidence != 0) + && (calibration.slope != 0) + && (calibration.intercept != 0)) { + return true; + } else { + return false; + } + } + + public void invalidate() { + this.slope_confidence = 0; + this.sensor_confidence = 0; + this.slope = 0; + this.intercept = 0; + save(); + PluggableCalibration.invalidateAllCaches(); + } + + public static synchronized void invalidateAllForSensor() { + final List cals = allForSensorLimited(9999999); + if (cals != null) { + for (Calibration cal : cals) { + cal.invalidate(); + } + } + JoH.clearCache(); + String msg = "Deleted all calibrations for sensor"; + Log.ueh(TAG, msg); + JoH.static_toast_long(msg); + } + +} + +abstract class SlopeParameters { + protected double LOW_SLOPE_1; + protected double LOW_SLOPE_2; + protected double HIGH_SLOPE_1; + protected double HIGH_SLOPE_2; + protected double DEFAULT_LOW_SLOPE_LOW; + protected double DEFAULT_LOW_SLOPE_HIGH; + protected int DEFAULT_SLOPE; + protected double DEFAULT_HIGH_SLOPE_HIGH; + protected double DEFAULT_HIGH_SLOPE_LOW; + + public double getLowSlope1() { + return LOW_SLOPE_1; + } + + public double getLowSlope2() { + return LOW_SLOPE_2; + } + + public double getHighSlope1() { + return HIGH_SLOPE_1; + } + + public double getHighSlope2() { + return HIGH_SLOPE_2; + } + + public double getDefaultLowSlopeLow() { + return DEFAULT_LOW_SLOPE_LOW; + } + + public double getDefaultLowSlopeHigh() { + return DEFAULT_LOW_SLOPE_HIGH; + } + + public int getDefaultSlope() { + return DEFAULT_SLOPE; + } + + public double getDefaultHighSlopeHigh() { + return DEFAULT_HIGH_SLOPE_HIGH; + } + + public double getDefaulHighSlopeLow() { + return DEFAULT_HIGH_SLOPE_LOW; + } + + public double restrictIntercept(double intercept) { return intercept; } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/CalibrationRequest.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/CalibrationRequest.java new file mode 100644 index 0000000..ea9d568 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/CalibrationRequest.java @@ -0,0 +1,87 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.Home; + +import java.util.List; + +/** + * Created by Emma Black on 12/9/14. + */ + +@Table(name = "CalibrationRequest", id = BaseColumns._ID) +public class CalibrationRequest extends Model { + private static final int max = 250; + private static final int min = 70; + private static final String TAG = CalibrationRequest.class.getSimpleName(); + + @Column(name = "requestIfAbove") + public double requestIfAbove; + + @Column(name = "requestIfBelow") + public double requestIfBelow; + + public static void createRange(double low, double high) { + CalibrationRequest calibrationRequest = new CalibrationRequest(); + calibrationRequest.requestIfAbove = low; + calibrationRequest.requestIfBelow = high; + calibrationRequest.save(); + } + static void createOffset(double center, double distance) { + CalibrationRequest calibrationRequest = new CalibrationRequest(); + calibrationRequest.requestIfAbove = center + distance; + calibrationRequest.requestIfBelow = max; + calibrationRequest.save(); + + calibrationRequest = new CalibrationRequest(); + calibrationRequest.requestIfAbove = min; + calibrationRequest.requestIfBelow = center - distance; + calibrationRequest.save(); + } + + static void clearAll(){ + List calibrationRequests = new Select() + .from(CalibrationRequest.class) + .execute(); + if (calibrationRequests.size() >=1) { + for (CalibrationRequest calibrationRequest : calibrationRequests) { + calibrationRequest.delete(); + } + } + } + + public static boolean shouldRequestCalibration(BgReading bgReading) { + CalibrationRequest calibrationRequest = new Select() + .from(CalibrationRequest.class) + .where("requestIfAbove < ?", bgReading.calculated_value) + .where("requestIfBelow > ?", bgReading.calculated_value) + .executeSingle(); + return (calibrationRequest != null && isSlopeFlatEnough(bgReading, 1)); + } + + public static boolean isSlopeFlatEnough() { + BgReading bgReading = BgReading.last(true); + if (bgReading == null) return false; + if (JoH.msSince(bgReading.timestamp) > Home.stale_data_millis()) { + UserError.Log.d(TAG, "Slope cannot be flat enough as data is stale"); + return false; + } + // TODO check if stale, check previous slope also, check that reading parameters also + return isSlopeFlatEnough(bgReading); + } + + public static boolean isSlopeFlatEnough(BgReading bgReading) { + return isSlopeFlatEnough(bgReading, 1); + } + + public static boolean isSlopeFlatEnough(BgReading bgReading, double limit) { + if (bgReading == null) return false; + // TODO use BestGlucose + return Math.abs(bgReading.calculated_value_slope * 60000) < limit; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/CobCalc.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/CobCalc.java new file mode 100644 index 0000000..0b37a79 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/CobCalc.java @@ -0,0 +1,11 @@ +package com.eveningoutpost.dexdrip.Models; + +/** + * Created by jamorham on 04/01/16. + */ +public class CobCalc { + double initialCarbs; + double decayedBy; + double isDecaying; + double carbTime; +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/DateUtil.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/DateUtil.java new file mode 100644 index 0000000..46cfe3b --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/DateUtil.java @@ -0,0 +1,96 @@ +package com.eveningoutpost.dexdrip.Models; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +// from package info.nightscout.client.utils; + +/** + * Created by mike on 30.12.2015. + */ + +/** + * The Class DateUtil. A simple wrapper around SimpleDateFormat to ease the handling of iso date string <-> date obj + * with TZ + */ +public class DateUtil { + + private static final String FORMAT_DATE_ISO = "yyyy-MM-dd'T'HH:mm:ss'Z'"; // eg 2017-03-24T22:03:27Z + private static final String FORMAT_DATE_ISO2 = "yyyy-MM-dd'T'HH:mm:ssZ"; // eg 2017-03-27T17:38:14+0300 + private static final String FORMAT_DATE_ISO3 = "yyyy-MM-dd'T'HH:mmZ"; // eg 2017-05-12T08:16-0400 + + /** + * Takes in an ISO date string of the following format: + * yyyy-mm-ddThh:mm:ss.ms+HoMo + * + * @param isoDateString the iso date string + * @return the date + * @throws Exception the exception + */ + private static Date fromISODateString(String isoDateString) + throws Exception { + SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO); + f.setTimeZone(TimeZone.getTimeZone("UTC")); + return f.parse(isoDateString); + } + + private static Date fromISODateString3(String isoDateString) + throws Exception { + SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO3); + f.setTimeZone(TimeZone.getTimeZone("UTC")); + return f.parse(isoDateString); + } + + private static Date fromISODateString2(String isoDateString) + throws Exception { + try { + SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO2); + f.setTimeZone(TimeZone.getTimeZone("UTC")); + return f.parse(isoDateString); + } catch (java.text.ParseException e) { + return fromISODateString3(isoDateString); + } + } + + public static Date tolerantFromISODateString(String isoDateString) + throws Exception { + try { + return fromISODateString(isoDateString.replaceFirst("\\.[0-9][0-9][0-9]Z$", "Z")); + } catch (java.text.ParseException e) { + return fromISODateString2(isoDateString); + } + } + + /** + * Render date + * + * @param date the date obj + * @param format - if not specified, will use FORMAT_DATE_ISO + * @param tz - tz to set to, if not specified uses local timezone + * @return the iso-formatted date string + */ + public static String toISOString(Date date, String format, TimeZone tz) { + if (format == null) format = FORMAT_DATE_ISO; + if (tz == null) tz = TimeZone.getDefault(); + DateFormat f = new SimpleDateFormat(format); + f.setTimeZone(tz); + return f.format(date); + } + + public static String toISOString(Date date) { + return toISOString(date, FORMAT_DATE_ISO, TimeZone.getTimeZone("UTC")); + } + + public static String toISOString(long date) { + return toISOString(new Date(date), FORMAT_DATE_ISO, TimeZone.getTimeZone("UTC")); + } + + public static String toNightscoutFormat(long date) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US); + format.setTimeZone(TimeZone.getDefault()); + return format.format(date); + } +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/DesertSync.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/DesertSync.java new file mode 100644 index 0000000..84042d3 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/DesertSync.java @@ -0,0 +1,574 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.os.Bundle; +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.GcmListenerSvc; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.JamListenerSvc; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; +import com.eveningoutpost.dexdrip.UtilityModels.desertsync.DesertComms; +import com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools; +import com.eveningoutpost.dexdrip.utils.CipherUtils; +import com.eveningoutpost.dexdrip.webservices.XdripWebService; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.firebase.messaging.RemoteMessage; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.Expose; +import com.google.gson.reflect.TypeToken; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import lombok.Builder; +import lombok.NoArgsConstructor; + +import static com.eveningoutpost.dexdrip.GoogleDriveInterface.getDriveIdentityString; +import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; +import static com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools.getBestInterfaceAddress; +import static com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools.ip; + +// created by jamorham 18/08/2018 + +// not to be confused with dessert sync, yum! + + +@NoArgsConstructor +@Table(name = "DesertSync", id = BaseColumns._ID) +public class DesertSync extends PlusModel { + + private static boolean patched = false; + private static final String TAG = DesertSync.class.getSimpleName(); + public static final String NO_DATA_MARKER = "NO DATA"; + private static final String PREF_SENDER_UUID = "DesertSync-sender-uuid"; + private static final int MAX_CATCHUP = 20; + private static final ReentrantLock sequence_lock = new ReentrantLock(); + private static final boolean d = false; + private static volatile int duplicateIndicator = 0; + private static volatile int catchupCounter = 0; + private static String static_sender = null; + private static RollCall myRollCall = null; + private static JamListenerSvc service; + private static HashMap peers; + private static int spinner = 0; + private static volatile String lastUsedIP = null; + + private static volatile long highestPullTimeStamp = -1; + + private static final String[] schema = { + "CREATE TABLE DesertSync (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE DesertSync ADD COLUMN timestamp INTEGER;", + "ALTER TABLE DesertSync ADD COLUMN topic TEXT;", + "ALTER TABLE DesertSync ADD COLUMN sender TEXT;", + "ALTER TABLE DesertSync ADD COLUMN payload TEXT;", + "ALTER TABLE DesertSync ADD COLUMN processed TEXT;", + "CREATE UNIQUE INDEX index_DesertSync_timestamp on DesertSync(timestamp);", + "CREATE INDEX index_DesertSync_payload on DesertSync(payload);", + "CREATE INDEX index_DesertSync_processed on DesertSync(processed);", + "CREATE INDEX index_DesertSync_topic on DesertSync(topic);"}; + + private static final int MAX_ITEMS = 50; + + public static final String PREF_WEBSERVICE_SECRET = "xdrip_webservice_secret"; + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "topic") + public String topic; + + @Expose + @Column(name = "sender") + public String sender; + + @Expose + @Column(name = "payload") + public String payload; + + @Column(name = "processed") + private String processed; + + + @Builder + private DesertSync(final long timestamp, final String topic, final String sender, final String payload, final boolean processedFlag) { + this.timestamp = timestamp; + this.topic = topic; + this.sender = sender; + if (processedFlag) { + this.processed = payload; + } else { + this.payload = payload; + } + } + + public static List since(final long position, final String topic) { + if (topic == null) { + return new Select() + .from(DesertSync.class) + .where("timestamp > ?", position) + .orderBy("timestamp asc") + .limit(MAX_ITEMS) + .execute(); + } else { + return new Select() + .from(DesertSync.class) + .where("topic = ?", topic) + .where("timestamp > ?", position) + .orderBy("timestamp asc") + .limit(MAX_ITEMS) + .execute(); + } + } + + private boolean alreadyInDatabase(final boolean processedFlag) { + return new Select() + .from(DesertSync.class) + .where("topic = ?", topic) + .where("processed = ?", processedFlag ? processed : processData()) + .executeSingle() != null; + } + + private static DesertSync last() { + return new Select() + .from(DesertSync.class) + .where("topic = ?", getTopic()) + .orderBy("timestamp desc") + .executeSingle(); + } + + + public String toS() { + return JoH.defaultGsonInstance().toJson(this); + } + + public String getAction() { + return getPayload(0); + } + + public String getPayload() { + return getPayload(1); + } + + private String processData() { + if (processed == null) { + processed = CipherUtils.decryptString(payload); + } + return processed; + } + + private String transmissionPayload() { + if (payload == null) { + payload = CipherUtils.compressEncryptString(processed); + } + return payload; + } + + private String getPayload(int section) { + if (processed == null) return ""; + processData(); + try { + final String[] ps = processed.split("\\^"); + return ps[section]; + } catch (Exception e) { + return ""; + } + } + + private RemoteMessage getMessage() { + final HashMap map = new HashMap<>(); + map.put("message", "From DesertSync"); + map.put("xfrom", sender); + map.put("yfrom", getYfrom()); + map.put("datum", getPayload()); + map.put("action", getAction()); + return new RemoteMessage.Builder("internal").setData(map).build(); + } + + // utility methods + + public static String toJson(List list) { + return JoH.defaultGsonInstance().toJson(list); + } + + public static boolean isEnabled() { + return Pref.getBooleanDefaultFalse("desert_sync_enabled"); + } + + // input / output + + public static void pullAsEnabled() { + if (Home.get_follower()) { + if (isEnabled()) { + // TODO check if no data received? or maybe we don't - should this instead be called from do nothing service?? + DesertComms.pullFromOasis(getTopic(), getHighestPullTimeStamp()); + } + } + } + + private synchronized static long getHighestPullTimeStamp() { + if (highestPullTimeStamp == -1) { + try { + highestPullTimeStamp = last().timestamp; + } catch (NullPointerException e) { + highestPullTimeStamp = 1; + } + } + return highestPullTimeStamp; + } + + private static DesertSync createFromBundle(final Bundle data) { + final String payload = data.getString("payload", data.getString("datum", "")); + if (payload.length() > 0) { + return new DesertSync(JoH.tsl(), data.getString("identity", getTopic()), mySender(), data.getString("action") + "^" + payload, true); + } else { + UserError.Log.d(TAG, "Invalid bundle"); + return null; + } + } + + public static boolean fromGCM(final Bundle data) { + if (isEnabled()) { + final DesertSync ds = createFromBundle(data); + if (ds != null && !ds.alreadyInDatabase(true)) { + DesertComms.pushToOasis(ds.topic, ds.sender, ds.transmissionPayload()); + ds.save(); + } else { + UserError.Log.d(TAG, "Not pushing entry without payload / duplicate"); + return false; + } + } + return true; + } + + public static boolean fromPush(String topic, String sender, String payload) { + if (isEnabled()) { + + UserError.Log.d(TAG, String.format("sender: %s, topic: %s, payload: %s", sender, topic, payload)); + if (sender == null || sender.length() != 32 || sender.equals(mySender())) return false; + if (topic == null || topic.length() != 32) return false; + if (payload == null || payload.length() == 0) return false; + // TODO VALIDATE PARAMS + + final DesertSync item = new DesertSync(JoH.tsl(), topic, sender, payload, false); + processItem(item); + return true; + } else { + return false; + } + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + private static void processItem(final DesertSync item) { + if (item != null) { + if (item.topic != null && item.topic.equals(getTopic())) { + if (!item.alreadyInDatabase(false)) { + UserError.Log.d(TAG, "New item: " + item.payload); + item.save(); + new Thread(() -> onMessageReceived(item.getMessage())).start(); + } else { + duplicateIndicator++; + UserError.Log.d(TAG, "Duplicate item: " + duplicateIndicator); + } + } else { + UserError.Log.d(TAG, "Invalid topic"); + } + } else { + UserError.Log.d(TAG, "processItem NULL"); + } + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + public static void fromPull(final String json) { + if (!json.startsWith(NO_DATA_MARKER)) { + try { + final List items = JoH.defaultGsonInstance().fromJson(json, new TypeToken>() { + }.getType()); + if (items != null) { + duplicateIndicator = 0; + for (final DesertSync item : items) { + if (item.timestamp > highestPullTimeStamp) { + highestPullTimeStamp = item.timestamp; + Inevitable.task("desert-sync-timestamp", 500, () -> { + UserError.Log.d(TAG, "Synced up till: " + JoH.dateTimeText(highestPullTimeStamp)); + }); + } + processItem(item); + } + if (items.size() == MAX_ITEMS) { + UserError.Log.d(TAG, "Attempting to catch up as all history is duplicates or max size: " + catchupCounter); + if (catchupCounter < MAX_CATCHUP) { + catchupCounter++; + Inevitable.task("Desert catchup", 6000, DesertSync::pullAsEnabled); + } + } else { + catchupCounter = 0; + } + } + } catch (JsonSyntaxException e) { + UserError.Log.e(TAG, "fromPull error: " + e + "\n" + json); + } + } else { + UserError.Log.d(TAG, "Web service reported no data matching our query - either we are synced or other mismatch"); + } + } + + public static void pullFailed(final String host) { + UserError.Log.d(TAG, "Pull failed: host: " + host); + if (host == null) return; + final String hint = RollCall.getBestMasterHintIP(); + UserError.Log.d(TAG, "Best hint: " + hint); + if (hint == null) return; + if (host.equals(hint)) { + UserError.Log.d(TAG, "Looking for hint but master is still the same: " + hint); + final String backupIP = DesertComms.getOasisBackupIP(); + if (!emptyString(backupIP) && !backupIP.equals(host)) { + UserError.Log.d(TAG, "Trying backup: " + backupIP); + takeMasterHint(backupIP); + } + } else { + UserError.Log.d(TAG, "Got master hint for: " + hint); + takeMasterHint(hint); + } + } + + private static void takeMasterHint(String hint) { + if (RouteTools.reachable(hint)) { + UserError.Log.d(TAG, "Master hint of: " + hint + " is reachable - setting up probe"); + DesertComms.probeOasis(getTopic(), hint); + } + } + + // identity + + private static final String PREF_LAST_DESERT_MY_IP = "last-desert-sync-my-ip"; + + public static void checkIpChange(final String result) { + // failed to reach peer + UserError.Log.d(TAG, "CheckIpChange enter: " + result); + if (result == null || (JoH.ratelimit("desert-check-ip-change", 60))) { + final String currentIP = getBestInterfaceAddress(); + UserError.Log.d(TAG, "check ip change: current: " + currentIP); + + if (!emptyString(currentIP)) { + if (lastUsedIP == null) { + lastUsedIP = PersistentStore.getString(PREF_LAST_DESERT_MY_IP); + } + UserError.Log.d(TAG, "check ip change last: " + lastUsedIP); + if (emptyString(lastUsedIP) || !currentIP.equals(lastUsedIP)) { + if (!emptyString(lastUsedIP)) { + UserError.Log.uel(TAG, "Our IP appears to have changed from: " + lastUsedIP + " to " + currentIP + " sending notification to peers"); + UserError.Log.d(TAG, "check ip change send ping"); + GcmActivity.desertPing(); + } + lastUsedIP = currentIP; + + PersistentStore.setString(PREF_LAST_DESERT_MY_IP, lastUsedIP); + } + } + } + } + + private static String getTopic() { + return getDriveIdentityString(); + } + + public static void masterIdReply(final String result, final String host) { + if (result == null) return; + if (Home.get_follower()) { + final RollCall rc = RollCall.fromJson(result); + if (rc == null) return; + if (rc.role.equals("Master")) { + DesertComms.setOasisIP(host); + pullAsEnabled(); + } + } else { + UserError.Log.e(TAG, "Refusing to process id reply as we are not a follower"); + } + + + } + + public static String mySender() { + if (static_sender == null) { + synchronized (DesertSync.class) { + if (static_sender == null) { + String sender = PersistentStore.getString(PREF_SENDER_UUID); + //UserError.Log.d(TAG, "From store: " + sender); + if (sender.length() != 32) { + sender = CipherUtils.getRandomHexKey(); + UserError.Log.d(TAG, "From key: " + sender); + PersistentStore.setString(PREF_SENDER_UUID, sender); + } + static_sender = sender; + } + } + } + UserError.Log.d(TAG, "Returning sender: " + static_sender); + return static_sender; + } + + public static String getMyRollCall(final String topic) { + if (topic != null && topic.equals(getTopic())) { + if (myRollCall == null || JoH.msSince(myRollCall.created) > Constants.MINUTE_IN_MS * 15) { + myRollCall = new RollCall(); + } + return myRollCall.populate().toS(); + } else { + return "Invalid topic"; + } + } + + // helpers + + private static JamListenerSvc getInstance() { + if (service == null) { + service = new GcmListenerSvc(); + service.setInjectable(); + } + return service; + } + + private static void onMessageReceived(final RemoteMessage message) { + if (sequence_lock.getQueueLength() > 0) { + UserError.Log.d(TAG, "Sequence lock has: " + sequence_lock.getQueueLength() + " waiting"); + } + try { + sequence_lock.tryLock(20, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // + } finally { + getInstance().onMessageReceived(message); + try { + sequence_lock.unlock(); + } catch (IllegalMonitorStateException e) { + // + } + } + } + + public static void learnPeer(final InetAddress address) { + if (peers == null) { + peers = new HashMap<>(); + } + if (!peers.containsKey(address)) { + if (RouteTools.isLocal(address)) { + UserError.Log.d(TAG, "Learned new peer: " + ip(address)); + } else { + UserError.Log.d(TAG, "Refusing to Learn new peer: " + ip(address)); + return; + } + } + peers.put(address, JoH.tsl()); + + spinner++; + if (spinner % 10 == 0) { + prunePeers(); + } + } + + private static void prunePeers() { + InetAddress toRemove = null; + for (final Map.Entry entry : peers.entrySet()) { + if (JoH.msSince(entry.getValue()) > Constants.DAY_IN_MS * 3) { + toRemove = entry.getKey(); + break; + } + } + if (toRemove != null) peers.remove(toRemove); + } + + public static List getActivePeers() { + final List list = new ArrayList<>(); + if (peers != null) { + for (final Map.Entry entry : peers.entrySet()) { + if (JoH.msSince(entry.getValue()) < Constants.HOUR_IN_MS * 3) { + list.add(ip(entry.getKey())); + } + } + } + return list; + } + + public static String getActivePeersString() { + final StringBuilder sb = new StringBuilder(); + for (final String str : getActivePeers()) { + sb.append(str); + sb.append(","); + } + return sb.toString(); + } + + private String getYfrom() { + return xdrip.gs(R.string.gcmtpc) + topic; + } + + public static void settingsChanged() { + if (isEnabled()) { + correctWebServiceSettings(); + } + } + + private static void correctWebServiceSettings() { + Pref.setBoolean("xdrip_webservice", true); + Pref.setBoolean("xdrip_webservice_open", true); + if (Pref.getString(PREF_WEBSERVICE_SECRET, "").length() == 0) { + Pref.setString(PREF_WEBSERVICE_SECRET, CipherUtils.getRandomHexKey()); + } + Inevitable.task("web service changed", 2000, XdripWebService::settingsChanged); + } + + // maintenance + + // create the table ourselves without worrying about model versioning and downgrading + public static void updateDB() { + patched = fixUpTable(schema, patched); + } + + public static void cleanup() { + try { + new Delete() + .from(DesertSync.class) + .where("timestamp < ?", JoH.tsl() - 86400000L) + .execute(); + } catch (Exception e) { + UserError.Log.d(TAG, "Exception cleaning uploader queue: " + e); + } + } + + public static void deleteAll() { + new Delete() + .from(DesertSync.class) + .execute(); + } + + // megastatus + + // data for MegaStatus + public static List megaStatus() { + final List l = new ArrayList<>(); + if (isEnabled()) { + if (Home.get_follower()) { + l.addAll(DesertComms.megaStatus()); + } + } + return l; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Forecast.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Forecast.java new file mode 100644 index 0000000..1ea072c --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Forecast.java @@ -0,0 +1,159 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.util.Log; + +import org.apache.commons.math3.linear.MatrixUtils; +import org.apache.commons.math3.linear.RealMatrix; +import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Created by jamorham on 08/02/2016. + */ +public class Forecast { + + private static final String TAG = "jamorham forecast"; + // from stackoverflow.com/questions/17592139/trend-lines-regression-curve-fitting-java-library + + public interface TrendLine { + void setValues(double[] y, double[] x); // y ~ f(x) + + double predict(double x); // get a predicted y for a given x + + double errorVarience(); + } + + public abstract static class OLSTrendLine implements TrendLine { + + RealMatrix coef = null; // will hold prediction coefs once we get values + Double last_error_rate = null; + + protected abstract double[] xVector(double x); // create vector of values from x + + protected abstract boolean logY(); // set true to predict log of y (note: y must be positive) + + + @Override + public void setValues(double[] y, double[] x) { + if (x.length != y.length) { + throw new IllegalArgumentException(String.format("The numbers of y and x values must be equal (%d != %d)", y.length, x.length)); + } + double[][] xData = new double[x.length][]; + for (int i = 0; i < x.length; i++) { + // the implementation determines how to produce a vector of predictors from a single x + xData[i] = xVector(x[i]); + } + if (logY()) { // in some models we are predicting ln y, so we replace each y with ln y + y = Arrays.copyOf(y, y.length); // user might not be finished with the array we were given + for (int i = 0; i < x.length; i++) { + y[i] = Math.log(y[i]); + } + } + final OLSMultipleLinearRegression ols = new OLSMultipleLinearRegression(); + ols.setNoIntercept(true); // let the implementation include a constant in xVector if desired + ols.newSampleData(y, xData); // provide the data to the model + coef = MatrixUtils.createColumnRealMatrix(ols.estimateRegressionParameters()); // get our coefs + last_error_rate = ols.estimateErrorVariance(); + Log.d(TAG, getClass().getSimpleName() + " Forecast Error rate: errorvar:" + + JoH.qs(last_error_rate, 4) + + " regssionvar:" + JoH.qs(ols.estimateRegressandVariance(), 4) + + " stderror:" + JoH.qs(ols.estimateRegressionStandardError(), 4)); + } + + @Override + public double predict(double x) { + double yhat = coef.preMultiply(xVector(x))[0]; // apply coefs to xVector + if (logY()) yhat = (Math.exp(yhat)); // if we predicted ln y, we still need to get y + return yhat; + } + + public static double[] toPrimitive(Double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return new double[0]; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + public static double[] toPrimitiveFromList(Collection array) { + if (array == null) { + return null; + } + + return toPrimitive(array.toArray(new Double[array.size()])); + } + + public double errorVarience() { + return last_error_rate; + } + + } + + public static class PolyTrendLine extends OLSTrendLine { + final int degree; + + public PolyTrendLine(int degree) { + if (degree < 0) + throw new IllegalArgumentException("The degree of the polynomial must not be negative"); + this.degree = degree; + } + + protected double[] xVector(double x) { // {1, x, x*x, x*x*x, ...} + double[] poly = new double[degree + 1]; + double xi = 1; + for (int i = 0; i <= degree; i++) { + poly[i] = xi; + xi *= x; + } + return poly; + } + + @Override + protected boolean logY() { + return false; + } + } + + public static class ExpTrendLine extends OLSTrendLine { + @Override + protected double[] xVector(double x) { + return new double[]{1, x}; + } + + @Override + protected boolean logY() { + return true; + } + } + + public static class PowerTrendLine extends OLSTrendLine { + @Override + protected double[] xVector(double x) { + return new double[]{1, Math.log(x)}; + } + + @Override + protected boolean logY() { + return true; + } + } + + public static class LogTrendLine extends OLSTrendLine { + @Override + protected double[] xVector(double x) { + return new double[]{1, Math.log(x)}; + } + + @Override + protected boolean logY() { + return false; + } + } +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/GlucoseData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/GlucoseData.java new file mode 100644 index 0000000..3500cf1 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/GlucoseData.java @@ -0,0 +1,38 @@ + +package com.eveningoutpost.dexdrip.Models; + +import java.text.DecimalFormat; + +// class from LibreAlarm + +public class GlucoseData implements Comparable { + + public long realDate; + public String sensorId; + public long sensorTime; + public int glucoseLevel = -1; + public int glucoseLevelRaw = -1; + public long phoneDatabaseId; + public int glucoseLevelRawSmoothed; + + public GlucoseData(){} + + // jamorham added constructor + public GlucoseData(int glucoseLevelRaw, long timestamp) { + this.glucoseLevelRaw = glucoseLevelRaw; + this.realDate = timestamp; + } + + public String glucose(boolean mmol) { + return glucose(glucoseLevel, mmol); + } + + public static String glucose(int mgdl, boolean mmol) { + return mmol ? new DecimalFormat("##.0").format(mgdl/18f) : String.valueOf(mgdl); + } + + @Override + public int compareTo(GlucoseData another) { + return (int) (realDate - another.realDate); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/HeartRate.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/HeartRate.java new file mode 100644 index 0000000..82d2633 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/HeartRate.java @@ -0,0 +1,129 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by jamorham on 01/11/2016. + */ + + +@Table(name = "HeartRate", id = BaseColumns._ID) +public class HeartRate extends Model { + + private final static String TAG = "HeartRate"; + private static boolean patched = false; + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "bpm") + public int bpm; + + @Expose + @Column(name = "accuracy") + public int accuracy; + + public static HeartRate last() { + try { + return new Select() + .from(HeartRate.class) + .orderBy("timestamp desc") + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static void create(long timestamp, int bpm, int accuracy) { + final HeartRate hr = new HeartRate(); + hr.timestamp = timestamp; + hr.bpm = bpm; + hr.accuracy = accuracy; + hr.saveit(); + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + // TODO efficient record creation? + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + return new Select() + .from(HeartRate.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return new ArrayList<>(); + } + } + + public static List cleanup(int retention_days) { + return new Delete() + .from(HeartRate.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L)) + .execute(); + } + + // create the table ourselves without worrying about model versioning and downgrading + private static void fixUpTable() { + if (patched) return; + String[] patchup = { + "CREATE TABLE HeartRate (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE HeartRate ADD COLUMN timestamp INTEGER;", + "ALTER TABLE HeartRate ADD COLUMN bpm INTEGER;", + "ALTER TABLE HeartRate ADD COLUMN accuracy INTEGER;", + "CREATE UNIQUE INDEX index_HeartRate_timestamp on HeartRate(timestamp);"}; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + // UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } + + // patches and saves + public Long saveit() { + fixUpTable(); + return save(); + } + + // TODO cache gson statically + public String toS() { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + return gson.toJson(this); + } +} + + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/InsulinInjection.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/InsulinInjection.java new file mode 100644 index 0000000..bf924c7 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/InsulinInjection.java @@ -0,0 +1,41 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.eveningoutpost.dexdrip.insulin.Insulin; +import com.eveningoutpost.dexdrip.insulin.InsulinManager; +import com.google.gson.annotations.Expose; + +import lombok.Getter; + +public class InsulinInjection { + private Insulin profile; + + @Expose + @Getter + private double units; + + @Expose + @Getter + private String insulin; + + public InsulinInjection(final Insulin p, final double u) { + profile = p; + units = u; + insulin = p.getName(); + } + + + public Insulin getProfile() { + // populate on demand + if (profile == null) { + profile = InsulinManager.getProfile(insulin); + } + return profile; + } + + // This is just a rough way to decide if it is a basal insulin without user needing to set it + // question as to whether this should be here or call to encapsulated method in Insulin + public boolean isBasal() { + return getProfile().getMaxEffect() > 1000; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Iob.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Iob.java new file mode 100644 index 0000000..96e8906 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Iob.java @@ -0,0 +1,16 @@ +package com.eveningoutpost.dexdrip.Models; + +/** + * Created by jamorham on 02/01/16. + */ +public class Iob { + public long timestamp; + public double iob = 0; + public CobCalc cobCalc; + public double cob = 0; + public double rawCarbImpact = 0; + public double jCarbImpact = 0; + public double jActivity = 0; +} + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/JoH.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/JoH.java new file mode 100644 index 0000000..1d1ce2e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/JoH.java @@ -0,0 +1,1690 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothManager; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.PowerManager; +import android.os.SystemClock; +import android.provider.Settings; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ContextThemeWrapper; +import android.text.InputType; +import android.text.method.DigitsKeyListener; +import android.util.Base64; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.View; +import android.widget.Toast; + +import com.activeandroid.ActiveAndroid; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.XdripNotificationCompat; +import com.eveningoutpost.dexdrip.utils.BestGZIPOutputStream; +import com.eveningoutpost.dexdrip.utils.CipherUtils; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedInts; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.zip.CRC32; +import java.util.zip.Checksum; +import java.util.zip.Deflater; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; + +import static android.bluetooth.BluetoothDevice.PAIRING_VARIANT_PIN; +import static android.content.Context.ALARM_SERVICE; +import static com.eveningoutpost.dexdrip.stats.StatsActivity.SHOW_STATISTICS_PRINT_COLOR; + +/** + * Created by jamorham on 06/01/16. + *

+ * lazy helper class for utilities + */ +public class JoH { + private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); + private final static String TAG = "jamorham JoH"; + private final static int PAIRING_VARIANT_PASSKEY = 1; // hidden in api + private final static boolean debug_wakelocks = false; + + private static double benchmark_time = 0; + private static Map benchmarks = new HashMap(); + private static final Map rateLimits = new HashMap<>(); + + public static boolean buggy_samsung = false; // flag set when we detect samsung devices which do not perform to android specifications + + // quick string conversion with leading zero + public static String qs0(double x, int digits) { + final String qs = qs(x, digits); + return qs.startsWith(".") ? "0" + qs : qs; + } + + // qs = quick string conversion of double for printing + public static String qs(double x) { + return qs(x, 2); + } + + // singletons to avoid repeated allocation + private static DecimalFormatSymbols dfs; + private static DecimalFormat df; + public static String qs(double x, int digits) { + + if (digits == -1) { + digits = 0; + if (((int) x != x)) { + digits++; + if ((((int) x * 10) / 10 != x)) { + digits++; + if ((((int) x * 100) / 100 != x)) digits++; + } + } + } + + if (dfs == null) { + final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); + local_dfs.setDecimalSeparator('.'); + dfs = local_dfs; // avoid race condition + } + + final DecimalFormat this_df; + // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe + if (Thread.currentThread().getId() == 1) { + if (df == null) { + final DecimalFormat local_df = new DecimalFormat("#", dfs); + local_df.setMinimumIntegerDigits(1); + df = local_df; // avoid race condition + } + this_df = df; + } else { + this_df = new DecimalFormat("#", dfs); + } + + this_df.setMaximumFractionDigits(digits); + return this_df.format(x); + } + + public static double ts() { + return new Date().getTime(); + } + + public static long tsl() { + return System.currentTimeMillis(); + } + + public static long uptime() { + return SystemClock.uptimeMillis(); + } + + public static boolean upForAtLeastMins(int mins) { + return uptime() > Constants.MINUTE_IN_MS * mins; + } + + public static long msSince(long when) { + return (tsl() - when); + } + + public static long msTill(long when) { + return (when - tsl()); + } + + public static long absMsSince(long when) { + return Math.abs(tsl() - when); + } + + public static String bytesToHex(byte[] bytes) { + if (bytes == null) return ""; + final char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + final int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] tolerantHexStringToByteArray(String str) { + return hexStringToByteArray(str.toUpperCase().replaceAll("[^A-F0-9]","")); + } + + public static byte[] hexStringToByteArray(String str) { + try { + str = str.toUpperCase().trim(); + if (str.length() == 0) return null; + final int len = str.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16)); + } + return data; + } catch (Exception e) { + Log.e(TAG, "Exception processing hexString: " + e); + return null; + } + } + + public static String macFormat(final String unformatted) { + if (unformatted == null) return null; + return unformatted.replaceAll("[^a-fA-F0-9]","").replaceAll("(.{2})", "$1:").substring(0,17); + } + + public static > SortedSet> mapSortedByValue(Map map, boolean descending) { + final SortedSet> sortedSet = new TreeSet<>((value1, value2) -> { + int result = descending ? value2.getValue().compareTo(value1.getValue()) + : value1.getValue().compareTo(value2.getValue()); + return result != 0 ? result : 1; + }); + sortedSet.addAll(map.entrySet()); + return sortedSet; + } + + + public static String compressString(String source) { + try { + + Deflater deflater = new Deflater(); + deflater.setInput(source.getBytes(Charset.forName("UTF-8"))); + deflater.finish(); + + byte[] buf = new byte[source.length() + 256]; + int count = deflater.deflate(buf); + // check count + deflater.end(); + return Base64.encodeToString(buf, 0, count, Base64.NO_WRAP); + } catch (Exception e) { + return null; + } + } + + public static byte[] compressStringToBytes(String string) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(string.length()); + BestGZIPOutputStream gzipped_data = new BestGZIPOutputStream(output); + gzipped_data.write(string.getBytes(Charset.forName("UTF-8"))); + gzipped_data.close(); + byte[] compressed = output.toByteArray(); + output.close(); + return compressed; + } catch (Exception e) { + Log.e(TAG, "Exception in compress: " + e.toString()); + return new byte[0]; + } + } + + public static byte[] compressBytesforPayload(byte[] bytes) { + return compressBytesToBytes(Bytes.concat(bytes, bchecksum(bytes))); + } + + public static byte[] compressBytesToBytes(byte[] bytes) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(bytes.length); + BestGZIPOutputStream gzipped_data = new BestGZIPOutputStream(output); + gzipped_data.write(bytes); + gzipped_data.close(); + byte[] compressed = output.toByteArray(); + output.close(); + return compressed; + } catch (Exception e) { + Log.e(TAG, "Exception in compress: " + e.toString()); + return new byte[0]; + } + } + + public static byte[] decompressBytesToBytes(byte[] bytes) { + try { + Log.d(TAG, "Decompressing bytes size: " + bytes.length); + byte[] buffer = new byte[8192]; + int bytes_read; + ByteArrayInputStream input = new ByteArrayInputStream(bytes); + ByteArrayOutputStream output = new ByteArrayOutputStream(bytes.length); + GZIPInputStream gzipped_data = new GZIPInputStream(input, buffer.length); + while ((bytes_read = gzipped_data.read(buffer)) != -1) { + output.write(buffer, 0, bytes_read); + } + gzipped_data.close(); + input.close(); + // output.close(); + return output.toByteArray(); + } catch (Exception e) { + Log.e(TAG, "Exception in decompress: " + e.toString()); + return new byte[0]; + } + } + + + public static String uncompressString(String input) { + try { + byte[] bytes = Base64.decode(input, Base64.NO_WRAP); + Inflater inflater = new Inflater(); + inflater.setInput(bytes); + inflater.finished(); + + byte[] buf = new byte[10000]; // max packet size because not using stream + int count = inflater.inflate(buf); + inflater.end(); + Log.d(TAG, "Inflated bytes: " + count); + return new String(buf, 0, count, "UTF-8"); + } catch (Exception e) { + Log.e(TAG, "Got exception uncompressing string"); + return null; + } + } + + public static String base64encode(String input) { + try { + return new String(Base64.encode(input.getBytes("UTF-8"), Base64.NO_WRAP), "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return "encode-error"; + } + } + + public static String base64decode(String input) { + try { + return new String(Base64.decode(input.getBytes("UTF-8"), Base64.NO_WRAP), "UTF-8"); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return "decode-error"; + } + } + + + public static String base64encodeBytes(byte[] input) { + try { + return new String(Base64.encode(input, Base64.NO_WRAP), "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return "encode-error"; + } + } + + public static byte[] base64decodeBytes(String input) { + try { + return Base64.decode(input.getBytes("UTF-8"), Base64.NO_WRAP); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return new byte[0]; + } + } + + + public static String ucFirst(String input) { + return input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase(); + } + + public static boolean isSamsung() { + return Build.MANUFACTURER.toLowerCase().contains("samsung"); + } + + private static final String BUGGY_SAMSUNG_ENABLED = "buggy-samsung-enabled"; + public static void persistentBuggySamsungCheck() { + if (!buggy_samsung) { + if (JoH.isSamsung() && PersistentStore.getLong(BUGGY_SAMSUNG_ENABLED) > 4) { + buggy_samsung = true; + UserError.Log.d(TAG,"Enabling buggy samsung mode due to historical pattern"); + } + } + } + + public static void setBuggySamsungEnabled() { + if (!buggy_samsung) { + JoH.buggy_samsung = true; + PersistentStore.incrementLong(BUGGY_SAMSUNG_ENABLED); + } + } + + + public static class DecimalKeyListener extends DigitsKeyListener { + private final char[] acceptedCharacters = + new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + new DecimalFormat().getDecimalFormatSymbols().getDecimalSeparator()}; + + @Override + protected char[] getAcceptedChars() { + return acceptedCharacters; + } + + public int getInputType() { + return InputType.TYPE_CLASS_NUMBER; + } + + } + + public static String backTrace() { + return backTrace(1); + } + + public static String backTrace(int depth) { + try { + StackTraceElement stack = new Exception().getStackTrace()[2 + depth]; + StackTraceElement stackb = new Exception().getStackTrace()[3 + depth]; + String[] stackclassa = stack.getClassName().split("\\."); + String[] stackbclassa = stackb.getClassName().split("\\."); + + return stackbclassa[stackbclassa.length - 1] + "::" + stackb.getMethodName() + + " -> " + stackclassa[stackclassa.length - 1] + "::" + stack.getMethodName(); + } catch (Exception e) { + return "unknown backtrace: " + e.toString(); + } + } + + public static String backTraceShort(int depth) { + try { + final StackTraceElement stackb = new Exception().getStackTrace()[3 + depth]; + return stackb.getMethodName(); + } catch (Exception e) { + return "unknown backtrace: " + e.toString(); + } + } + + public static void benchmark(String name) { + if (name == null) { + if (benchmark_time == 0) { + benchmark_time = ts(); + } else { + Log.e(TAG, "Cannot start a benchmark as one is already running - cancelling"); + benchmark_time = 0; + } + } else { + if (benchmark_time == 0) { + Log.e(TAG, "Benchmark: " + name + " no benchmark set!"); + } else { + Log.i(TAG, "Benchmark: " + name + " " + (ts() - benchmark_time) + " ms"); + benchmark_time = 0; + } + } + } + + public static void dumpBundle(Bundle bundle, String tag) { + if (bundle != null) { + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value != null) { + UserError.Log.d(tag, String.format("%s %s (%s)", key, + value.toString(), value.getClass().getName())); + } + } + } else { + UserError.Log.d(tag, "Bundle is empty"); + } + } + + + // compare stored byte array hashes + public static synchronized boolean differentBytes(String name, byte[] bytes) { + final String id = "differentBytes-" + name; + final String last_hash = PersistentStore.getString(id); + final String this_hash = CipherUtils.getSHA256(bytes); + if (this_hash.equals(last_hash)) return false; + PersistentStore.setString(id, this_hash); + return true; + } + + public static synchronized void clearRatelimit(final String name) { + if (PersistentStore.getLong(name) > 0) { + PersistentStore.setLong(name, 0); + } + if (rateLimits.containsKey(name)) { + rateLimits.remove(name); + } + } + + // return true if below rate limit (persistent version) + public static synchronized boolean pratelimit(String name, int seconds) { + // check if over limit + final long time_now = JoH.tsl(); + final long rate_time; + if (!rateLimits.containsKey(name)) { + rate_time = PersistentStore.getLong(name); // 0 if undef + } else { + rate_time = rateLimits.get(name); + } + if ((rate_time > 0) && (time_now - rate_time) < (seconds * 1000L)) { + Log.d(TAG, name + " rate limited: " + seconds + " seconds"); + return false; + } + // not over limit + rateLimits.put(name, time_now); + PersistentStore.setLong(name, time_now); + return true; + } + + // return true if below rate limit + public static synchronized boolean ratelimit(String name, int seconds) { + // check if over limit + if ((rateLimits.containsKey(name)) && (JoH.tsl() - rateLimits.get(name) < (seconds * 1000L))) { + Log.d(TAG, name + " rate limited: " + seconds + " seconds"); + return false; + } + // not over limit + rateLimits.put(name, JoH.tsl()); + return true; + } + + // return true if below rate limit + public static synchronized boolean quietratelimit(String name, int seconds) { + // check if over limit + if ((rateLimits.containsKey(name)) && (JoH.tsl() - rateLimits.get(name) < (seconds * 1000))) { + return false; + } + // not over limit + rateLimits.put(name, JoH.tsl()); + return true; + } + + // return true if below rate limit + public static synchronized boolean ratelimitmilli(String name, int milliseconds) { + // check if over limit + if ((rateLimits.containsKey(name)) && (JoH.tsl() - rateLimits.get(name) < (milliseconds))) { + Log.d(TAG, name + " rate limited: " + milliseconds + " milliseconds"); + return false; + } + // not over limit + rateLimits.put(name, JoH.tsl()); + return true; + } + + public static String getDeviceDetails() { + final String manufacturer = Build.MANUFACTURER.replace(" ", "_"); + final String model = Build.MODEL.replace(" ", "_"); + final String version = Integer.toString(Build.VERSION.SDK_INT) + " " + Build.VERSION.RELEASE + " " + Build.VERSION.INCREMENTAL; + return manufacturer + " " + model + " " + version; + } + + public static String getVersionDetails() { + try { + return xdrip.getAppContext().getPackageManager().getPackageInfo(xdrip.getAppContext().getPackageName(), PackageManager.GET_META_DATA).versionName; + } catch (Exception e) { + return "Unknown version"; + } + } + + public static boolean isOldVersion(Context context) { + try { + final Signature[] pinfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures; + if (pinfo.length == 1) { + final Checksum s = new CRC32(); + final byte[] ba = pinfo[0].toByteArray(); + s.update(ba, 0, ba.length); + if (s.getValue() == 2009579833) return true; + } + } catch (Exception e) { + Log.d(TAG, "exception: " + e); + } + return false; + } + + public static boolean getWifiSleepPolicyNever() { + try { + int policy = Settings.Global.getInt(xdrip.getAppContext().getContentResolver(), android.provider.Settings.Global.WIFI_SLEEP_POLICY); + Log.d(TAG, "Current WifiPolicy: " + ((policy == Settings.Global.WIFI_SLEEP_POLICY_NEVER) ? "Never" : Integer.toString(policy)) + " " + Settings.Global.WIFI_SLEEP_POLICY_DEFAULT + " " + Settings.Global.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED); + return (policy == Settings.Global.WIFI_SLEEP_POLICY_NEVER); + } catch (Exception e) { + Log.e(TAG, "Exception during global settings policy"); + return true; // we don't know anything + } + } + + public static void benchmark_method_start() { + benchmarks.put(backTrace(0), ts()); + } + + public static void benchmark_method_end() { + String name = backTrace(0); + try { + + double timing = ts() - benchmarks.get(name); + Log.i(TAG, "Benchmark: " + name + " " + timing + "ms"); + } catch (Exception e) { + Log.e(TAG, "Benchmark: " + name + " no benchmark set!"); + } + } + + public static void fixActionBar(AppCompatActivity context) { + try { + context.getSupportActionBar().setDisplayShowHomeEnabled(true); + context.getSupportActionBar().setIcon(R.drawable.ic_launcher); + } catch (Exception e) { + Log.e(TAG, "Got exception with supportactionbar: " + e.toString()); + + } + } + + public static HashMap JsonStringtoMap(String json) { + return new Gson().fromJson(json, new TypeToken>() { + }.getType()); + } + + private static Gson gson_instance; + public static Gson defaultGsonInstance() { + if (gson_instance == null) { + gson_instance = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + //.registerTypeAdapter(Date.class, new DateTypeAdapter()) + // .serializeSpecialFloatingPointValues() + .create(); + } + return gson_instance; + } + + public static String hourMinuteString() { + // Date date = new Date(); + // SimpleDateFormat sd = new SimpleDateFormat("HH:mm"); + // return sd.format(date); + return hourMinuteString(JoH.tsl()); + } + + public static String hourMinuteString(long timestamp) { + return android.text.format.DateFormat.format("kk:mm", timestamp).toString(); + } + + public static String dateTimeText(long timestamp) { + return android.text.format.DateFormat.format("yyyy-MM-dd kk:mm:ss", timestamp).toString(); + } + + public static String dateText(long timestamp) { + return android.text.format.DateFormat.format("yyyy-MM-dd", timestamp).toString(); + } + + public static long getTimeZoneOffsetMs() { + return new GregorianCalendar().getTimeZone().getRawOffset(); + } + + public static String niceTimeSince(long t) { + return niceTimeScalar(msSince(t)); + } + + public static String niceTimeTill(long t) { + return niceTimeScalar(-msSince(t)); + } + + // temporary + public static String niceTimeScalar(long t) { + String unit = xdrip.getAppContext().getString(R.string.unit_second); + t = t / 1000; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_seconds); + if (t > 59) { + unit = xdrip.getAppContext().getString(R.string.unit_minute); + t = t / 60; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_minutes); + if (t > 59) { + unit = xdrip.getAppContext().getString(R.string.unit_hour); + t = t / 60; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_hours); + if (t > 24) { + unit = xdrip.getAppContext().getString(R.string.unit_day); + t = t / 24; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_days); + if (t > 28) { + unit = xdrip.getAppContext().getString(R.string.unit_week); + t = t / 7; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_weeks); + } + } + } + } + //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character + return qs((double) t, 0) + " " + unit; + } + + public static String niceTimeScalar(double t, int digits) { + String unit = xdrip.getAppContext().getString(R.string.unit_second); + t = t / 1000; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_seconds); + if (t > 59) { + unit = xdrip.getAppContext().getString(R.string.unit_minute); + t = t / 60; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_minutes); + if (t > 59) { + unit = xdrip.getAppContext().getString(R.string.unit_hour); + t = t / 60; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_hours); + if (t > 24) { + unit = xdrip.getAppContext().getString(R.string.unit_day); + t = t / 24; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_days); + if (t > 28) { + unit = xdrip.getAppContext().getString(R.string.unit_week); + t = t / 7; + if (t != 1) unit = xdrip.getAppContext().getString(R.string.unit_weeks); + } + } + } + } + //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character + return qs( t, digits) + " " + unit; + } + + + public static String niceTimeScalarNatural(long t) { + if (t > 3000000) t = t + 10000; // round up by 10 seconds if nearly an hour + if ((t > Constants.DAY_IN_MS) && (t < Constants.WEEK_IN_MS * 2)) { + final SimpleDateFormat df = new SimpleDateFormat("EEEE", Locale.getDefault()); + final String day = df.format(new Date(JoH.tsl() + t)); + return ((t > Constants.WEEK_IN_MS) ? "next " : "") + day; + } else { + return niceTimeScalar(t); + } + } + + public static String niceTimeScalarRedux(long t) { + return niceTimeScalar(t).replaceFirst("^1 ", ""); + } + + public static String niceTimeScalarShort(long t) { + return niceTimeScalar(t).replaceFirst("([A-z]).*$", "$1"); + } + + public static String niceTimeScalarShortWithDecimalHours(long t) { + if (t > Constants.HOUR_IN_MS) { + return niceTimeScalar(t,1).replaceFirst("([A-z]).*$", "$1"); + } else { + return niceTimeScalar(t).replaceFirst("([A-z]).*$", "$1"); + } + } + + + public static double tolerantParseDouble(String str) throws NumberFormatException { + return Double.parseDouble(str.replace(",", ".")); + } + + public static double tolerantParseDouble(final String str, final double def) { + if (str == null) return def; + try { + return Double.parseDouble(str.replace(",", ".")); + } catch (NumberFormatException e) { + return def; + } + } + + public static int tolerantParseInt(final String str, final int def) { + if (str == null) return def; + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return def; + } + } + + public static long tolerantParseLong(final String str, final long def) { + if (str == null) return def; + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return def; + } + } + + + public static String getRFC822String(long timestamp) { + final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + return dateFormat.format(new Date(timestamp)); + } + + public static PowerManager.WakeLock getWakeLock(final String name, int millis) { + final PowerManager pm = (PowerManager) xdrip.getAppContext().getSystemService(Context.POWER_SERVICE); + final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wl.acquire(millis); + if (debug_wakelocks) Log.d(TAG, "getWakeLock: " + name + " " + wl.toString()); + return wl; + } + + public static void releaseWakeLock(PowerManager.WakeLock wl) { + if (debug_wakelocks) Log.d(TAG, "releaseWakeLock: " + wl.toString()); + if (wl == null) return; + if (wl.isHeld()) { + try { + wl.release(); + } catch (Exception e) { + Log.e(TAG, "Error releasing wakelock: " + e); + } + } + } + + public static PowerManager.WakeLock fullWakeLock(final String name, long millis) { + final PowerManager pm = (PowerManager) xdrip.getAppContext().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, name); + wl.acquire(millis); + if (debug_wakelocks) Log.d(TAG, "fullWakeLock: " + name + " " + wl.toString()); + return wl; + } + + public static void fullDatabaseReset() { + try { + clearCache(); + ActiveAndroid.dispose(); + ActiveAndroid.initialize(xdrip.getAppContext()); + } catch (Exception e) { + Log.e(TAG,"Error restarting active android db"); + } + } + + public static void clearCache() { + try { + ActiveAndroid.clearCache(); + } catch (Exception e) { + Log.e(TAG, "Error clearing active android cache: " + e); + } + } + + public static boolean isLANConnected() { + final ConnectivityManager cm = + (ConnectivityManager) xdrip.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + final boolean isConnected = activeNetwork != null && + activeNetwork.isConnected(); + return isConnected && ((activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) + || (activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET) + || (activeNetwork.getType() == ConnectivityManager.TYPE_BLUETOOTH)); + } + + public static boolean isMobileDataOrEthernetConnected() { + final ConnectivityManager cm = + (ConnectivityManager) xdrip.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + final boolean isConnected = activeNetwork != null && + activeNetwork.isConnected(); + return isConnected && ((activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) || (activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET)); + } + + public static boolean isAnyNetworkConnected() { + final ConnectivityManager cm = + (ConnectivityManager) xdrip.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && + activeNetwork.isConnected(); + } + + public static boolean isScreenOn() { + final PowerManager pm = (PowerManager) xdrip.getAppContext().getSystemService(Context.POWER_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return pm.isInteractive(); + } else { + return pm.isScreenOn(); + } + } + + public static boolean isOngoingCall() { + try { + AudioManager manager = (AudioManager) xdrip.getAppContext().getSystemService(Context.AUDIO_SERVICE); + return (manager.getMode() == AudioManager.MODE_IN_CALL); + // possibly should have MODE_IN_COMMUNICATION as well + } catch (Exception e) { + return false; + } + } + + public static String getWifiSSID() { + try { + final WifiManager wifi_manager = (WifiManager) xdrip.getAppContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifi_manager.isWifiEnabled()) { + final WifiInfo wifiInfo = wifi_manager.getConnectionInfo(); + if (wifiInfo != null) { + final NetworkInfo.DetailedState wifi_state = WifiInfo.getDetailedStateOf(wifiInfo.getSupplicantState()); + if (wifi_state == NetworkInfo.DetailedState.CONNECTED + || wifi_state == NetworkInfo.DetailedState.OBTAINING_IPADDR + || wifi_state == NetworkInfo.DetailedState.CAPTIVE_PORTAL_CHECK) { + String ssid = wifiInfo.getSSID(); + if (ssid.equals("")) return null; // WifiSsid.NONE; + if (ssid.charAt(0) == '"') ssid = ssid.substring(1); + if (ssid.charAt(ssid.length() - 1) == '"') + ssid = ssid.substring(0, ssid.length() - 1); + return ssid; + } + } + } + } catch (Exception e) { + Log.e(TAG, "Got exception in getWifiSSID: " + e); + } + return null; + } + + public static boolean getWifiFuzzyMatch(String local, String remote) { + if ((local == null) || (remote == null) || (local.length() == 0) || (remote.length() == 0)) + return false; + final int slen = Math.min(local.length(), remote.length()); + final int llen = Math.max(local.length(), remote.length()); + int matched = 0; + for (int i = 0; i < slen; i++) { + if (local.charAt(i) == (remote.charAt(i))) matched++; + } + boolean result = false; + if (matched == slen) result = true; // shorter string is substring + final double quota = (double) matched / (double) llen; + final int dmatch = llen - matched; + if (slen > 2) { + if (dmatch < 3) result = true; + if (quota > 0.80) result = true; + } + //Log.d(TAG, "l:" + local + " r:" + remote + " slen:" + slen + " llen:" + llen + " matched:" + matched + " q:" + JoH.qs(quota, 2) + " dm:" + dmatch + " RESULT: " + result); + return result; + } + + public static boolean runOnUiThread(Runnable theRunnable) { + final Handler mainHandler = new Handler(xdrip.getAppContext().getMainLooper()); + return mainHandler.post(theRunnable); + } + + public static boolean runOnUiThreadDelayed(Runnable theRunnable, long delay) { + final Handler mainHandler = new Handler(xdrip.getAppContext().getMainLooper()); + return mainHandler.postDelayed(theRunnable, delay); + } + + public static void removeUiThreadRunnable(Runnable theRunnable) { + final Handler mainHandler = new Handler(xdrip.getAppContext().getMainLooper()); + mainHandler.removeCallbacks(theRunnable); + } + + public static void hardReset() { + try { + android.os.Process.killProcess(android.os.Process.myPid()); + } catch (Exception e) { + // not much to do + } + } + + public static void static_toast(final Context context, final String msg, final int length) { + try { + if (!runOnUiThread(new Runnable() { + @Override + public void run() { + try { + Toast.makeText(context, msg, length).show(); + Log.i(TAG, "Displaying toast using fallback"); + } catch (Exception e) { + Log.e(TAG, "Exception processing runnable toast ui thread: " + e); + Home.toaststatic(msg); + } + } + })) { + Log.e(TAG, "Couldn't display toast via ui thread: " + msg); + Home.toaststatic(msg); + } + } catch (Exception e) { + Log.e(TAG, "Couldn't display toast due to exception: " + msg + " e: " + e.toString()); + Home.toaststatic(msg); + } + } + + public static void static_toast_long(final String msg) { + static_toast(xdrip.getAppContext(), msg, Toast.LENGTH_LONG); + } + + public static void static_toast_short(final String msg) { + static_toast(xdrip.getAppContext(), msg, Toast.LENGTH_SHORT); + } + + public static void static_toast_long(Context context, final String msg) { + static_toast(context, msg, Toast.LENGTH_LONG); + } + + public static void show_ok_dialog(final Activity activity, final String title, final String message, final Runnable runnable) { + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme)); + builder.setTitle(title); + builder.setMessage(message); + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + try { + dialog.dismiss(); + } catch (Exception e) { + // + } + if (runnable != null) { + runOnUiThreadDelayed(runnable, 10); + } + } + }); + + builder.create().show(); + } catch (Exception e) { + Log.wtf(TAG, "show_dialog exception: " + e); + static_toast_long(message); + } + } + }); + } + + public static synchronized void playResourceAudio(int id) { + playSoundUri(getResourceURI(id)); + } + + public static String getResourceURI(int id) { + return ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + xdrip.getAppContext().getPackageName() + "/" + id; + } + + public static synchronized MediaPlayer playSoundUri(String soundUri) { + try { + JoH.getWakeLock("joh-playsound", 10000); + final MediaPlayer player = MediaPlayer.create(xdrip.getAppContext(), Uri.parse(soundUri)); + player.setLooping(false); + player.start(); + return player; + } catch (Exception e) { + Log.wtf(TAG, "Failed to play audio: " + soundUri + " exception:" + e); + return null; + } + } + + public static boolean validateMacAddress(final String mac) { + return mac != null && mac.length() == 17 && mac.matches("([\\da-fA-F]{1,2}(?:\\:|$)){6}"); + } + + public static String urlEncode(String source) { + try { + return URLEncoder.encode(source, "UTF-8"); + } catch (Exception e) { + return "encoding-exception"; + } + } + + public static Object cloneObject(Object obj) { + try { + Object clone = obj.getClass().newInstance(); + for (Field field : obj.getClass().getDeclaredFields()) { + field.setAccessible(true); + field.set(clone, field.get(obj)); + } + return clone; + } catch (Exception e) { + return null; + } + } + + public static void stopService(Class c) { + xdrip.getAppContext().stopService(new Intent(xdrip.getAppContext(), c)); + } + + public static void startService(Class c) { + xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), c)); + } + + public static void startService(final Class c, final String... args) { + startService(c, null, args); + } + + public static void startService(final Class c, final byte[] bytes, final String... args) { + final Intent intent = new Intent(xdrip.getAppContext(), c); + if (bytes != null) { + intent.putExtra("bytes_payload", bytes); + } + if (args.length % 2 == 1) { + throw new RuntimeException("Odd number of args for JoH.startService"); + } + for (int i = 0; i < args.length; i += 2) { + intent.putExtra(args[i], args[i + 1]); + } + xdrip.getAppContext().startService(intent); + } + + + public static void startActivity(Class c) { + xdrip.getAppContext().startActivity(getStartActivityIntent(c)); + } + + + public static Intent getStartActivityIntent(Class c) { + return new Intent(xdrip.getAppContext(), c).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + + public static void goFullScreen(boolean fullScreen, View decorView) { + + if (fullScreen) { + if (Build.VERSION.SDK_INT >= 19) { + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + } else { + decorView.setSystemUiVisibility(0); // TODO will this need revisiting in later android vers? + } + } + + + public static Bitmap screenShot(View view, String annotation) { + + if (view == null) { + static_toast_long("View is null in screenshot!"); + return null; + } + final int width = view.getWidth(); + final int height = view.getHeight(); + Log.d(TAG, "Screenshot called: " + width + "," + height); + final Bitmap bitmap = Bitmap.createBitmap(width, + height, Bitmap.Config.ARGB_8888); + + final Canvas canvas = new Canvas(bitmap); + if (Pref.getBooleanDefaultFalse(SHOW_STATISTICS_PRINT_COLOR)) { + Paint paint = new Paint(); + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.FILL); + canvas.drawRect(0, 0, width, height, paint); + } + + + view.destroyDrawingCache(); + view.layout(0, 0, width, height); + view.draw(canvas); + + if (annotation != null) { + final int offset = (annotation != null) ? 40 : 0; + final Bitmap bitmapf = Bitmap.createBitmap(width, + height + offset, Bitmap.Config.ARGB_8888); + final Canvas canvasf = new Canvas(bitmapf); + + Paint paint = new Paint(); + if (Pref.getBooleanDefaultFalse(SHOW_STATISTICS_PRINT_COLOR)) { + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.FILL); + canvasf.drawRect(0, 0, width, offset, paint); + paint.setColor(Color.BLACK); + } else { + paint.setColor(Color.GRAY); + } + paint.setTextSize(20); + // paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)); + canvasf.drawBitmap(bitmap, 0, offset, paint); + canvasf.drawText(annotation, 50, (offset / 2) + 5, paint); + bitmap.recycle(); + return bitmapf; + } + + return bitmap; + } + + public static Bitmap screenShot2(View view) { + Log.d(TAG, "Screenshot2 called: " + view.getWidth() + "," + view.getHeight()); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(true); + final Bitmap bitmap = view.getDrawingCache(true); + return bitmap; + } + + + public static void bitmapToFile(Bitmap bitmap, String path, String fileName) { + + if (bitmap == null) return; + Log.d(TAG, "bitmapToFile: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + File dir = new File(path); + if (!dir.exists()) + dir.mkdirs(); + final File file = new File(path, fileName); + try { + FileOutputStream output = new FileOutputStream(file); + final boolean result = bitmap.compress(Bitmap.CompressFormat.PNG, 80, output); + output.flush(); + output.close(); + Log.d(TAG, "Bitmap compress result: " + result); + } catch (Exception e) { + Log.e(TAG, "Got exception writing bitmap to file: " + e); + } + } + + public static void shareImage(Context context, File file) { + Uri uri = Uri.fromFile(file); + final Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("image/*"); + intent.putExtra(android.content.Intent.EXTRA_SUBJECT, ""); + intent.putExtra(android.content.Intent.EXTRA_TEXT, ""); + intent.putExtra(Intent.EXTRA_STREAM, uri); + try { + context.startActivity(Intent.createChooser(intent, "Share")); + } catch (ActivityNotFoundException e) { + static_toast_long("No suitable app to show an image!"); + } + } + + public static void cancelAlarm(Context context, PendingIntent serviceIntent) { + // do we want a try catch block here? + final AlarmManager alarm = (AlarmManager) context.getSystemService(ALARM_SERVICE); + if (serviceIntent != null) { + Log.d(TAG, "Cancelling alarm " + serviceIntent.getCreatorPackage()); + alarm.cancel(serviceIntent); + } else { + Log.d(TAG, "Cancelling alarm: serviceIntent is null"); + } + } + + public static long wakeUpIntent(Context context, long delayMs, PendingIntent pendingIntent) { + final long wakeTime = JoH.tsl() + delayMs; + if (pendingIntent != null) { + Log.d(TAG, "Scheduling wakeup intent: " + dateTimeText(wakeTime)); + final AlarmManager alarm = (AlarmManager) context.getSystemService(ALARM_SERVICE); + try { + alarm.cancel(pendingIntent); + } catch (Exception e) { + Log.e(TAG, "Exception cancelling alarm in wakeUpIntent: " + e); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (buggy_samsung && Pref.getBoolean("allow_samsung_workaround", true)) { + alarm.setAlarmClock(new AlarmManager.AlarmClockInfo(wakeTime, null), pendingIntent); + } else { + alarm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarm.setExact(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent); + } else + alarm.set(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent); + } else { + Log.e(TAG, "wakeUpIntent - pending intent was null!"); + } + return wakeTime; + } + + public static void scheduleNotification(Context context, String title, String body, int delaySeconds, int notification_id) { + final Intent notificationIntent = new Intent(context, Home.class).putExtra(Home.SHOW_NOTIFICATION, title).putExtra("notification_body", body).putExtra("notification_id", notification_id); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, notification_id, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + Log.d(TAG, "Scheduling notification: " + title + " / " + body); + wakeUpIntent(context, delaySeconds * 1000, pendingIntent); + } + + public static void cancelNotification(int notificationId) { + try { + final NotificationManager mNotifyMgr = (NotificationManager) xdrip.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); + mNotifyMgr.cancel(notificationId); + } catch (Exception e) { + // + } + } + + public static void showNotification(String title, String content, PendingIntent intent, int notificationId, boolean sound, boolean vibrate, boolean onetime) { + showNotification(title, content, intent, notificationId, sound, vibrate, null, null); + } + + public static void showNotification(String title, String content, PendingIntent intent, int notificationId, boolean sound, boolean vibrate, PendingIntent deleteIntent, Uri sound_uri) { + showNotification(title, content, intent, notificationId, null, sound, vibrate, deleteIntent, sound_uri, null); + } + + public static void showNotification(String title, String content, PendingIntent intent, int notificationId, boolean sound, boolean vibrate, PendingIntent deleteIntent, Uri sound_uri, String bigmsg) { + showNotification(title, content, intent, notificationId, null, sound, vibrate, deleteIntent, sound_uri, bigmsg); + } + + public static void showNotification(String title, String content, PendingIntent intent, int notificationId, String channelId, boolean sound, boolean vibrate, PendingIntent deleteIntent, Uri sound_uri, String bigmsg) { + final NotificationCompat.Builder mBuilder = notificationBuilder(title, content, intent, channelId); + final long[] vibratePattern = {0, 1000, 300, 1000, 300, 1000}; + if (vibrate) mBuilder.setVibrate(vibratePattern); + if (deleteIntent != null) mBuilder.setDeleteIntent(deleteIntent); + mBuilder.setLights(0xff00ff00, 300, 1000); + if (sound) { + Uri soundUri = (sound_uri != null) ? sound_uri : RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + mBuilder.setSound(soundUri); + } + + if (bigmsg != null) { + mBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigmsg)); + } + + final NotificationManager mNotifyMgr = (NotificationManager) xdrip.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); + // if (!onetime) mNotifyMgr.cancel(notificationId); + + mNotifyMgr.notify(notificationId, XdripNotificationCompat.build(mBuilder)); + } + + private static NotificationCompat.Builder notificationBuilder(String title, String content, PendingIntent intent, String channelId) { + return new NotificationCompat.Builder(xdrip.getAppContext(), channelId) + .setSmallIcon(R.drawable.ic_action_communication_invert_colors_on) + .setContentTitle(title) + .setContentText(content) + .setContentIntent(intent); + } + + + public static void releaseOrientation(Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + public static void lockOrientation(Activity activity) { + Display display = activity.getWindowManager().getDefaultDisplay(); + int rotation = display.getRotation(); + int height; + int width; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) { + height = display.getHeight(); + width = display.getWidth(); + } else { + Point size = new Point(); + display.getSize(size); + height = size.y; + width = size.x; + } + switch (rotation) { + case Surface.ROTATION_90: + if (width > height) + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + else + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); + break; + case Surface.ROTATION_180: + if (height > width) + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); + else + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + break; + case Surface.ROTATION_270: + if (width > height) + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + else + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + break; + default: + if (height > width) + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + else + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } + } + + public static boolean areWeRunningOnAndroidWear() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH + && (xdrip.getAppContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_WATCH; + } + + public static boolean isAirplaneModeEnabled(Context context) { + return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0; + } + + + public static byte[] convertPinToBytes(String pin) { + if (pin == null) { + return null; + } + byte[] pinBytes; + try { + pinBytes = pin.getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + Log.e(TAG, "UTF-8 not supported?!?"); // this should not happen + return null; + } + if (pinBytes.length <= 0 || pinBytes.length > 16) { + return null; + } + return pinBytes; + } + + public static boolean doPairingRequest(Context context, BroadcastReceiver broadcastReceiver, Intent intent, String mBluetoothDeviceAddress) { + return doPairingRequest(context, broadcastReceiver, intent, mBluetoothDeviceAddress, null); + } + + @TargetApi(19) + public static boolean doPairingRequest(Context context, BroadcastReceiver broadcastReceiver, Intent intent, final String mBluetoothDeviceAddress, final String pinHint) { + if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) { + final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device != null) { + int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); + if ((mBluetoothDeviceAddress != null) && (device.getAddress().equals(mBluetoothDeviceAddress))) { + + if (type == PAIRING_VARIANT_PASSKEY && pinHint != null) { + return false; + } + + if ((type == PAIRING_VARIANT_PIN) && (pinHint != null)) { + device.setPin(convertPinToBytes(pinHint)); + Log.d(TAG, "Setting pairing pin to " + pinHint); + broadcastReceiver.abortBroadcast(); + } + try { + UserError.Log.e(TAG, "Pairing type: " + type); + if (type != PAIRING_VARIANT_PIN && type != PAIRING_VARIANT_PASSKEY) { + device.setPairingConfirmation(true); + JoH.static_toast_short("xDrip Pairing"); + broadcastReceiver.abortBroadcast(); + } else { + Log.d(TAG, "Attempting to passthrough PIN pairing"); + } + + } catch (Exception e) { + UserError.Log.e(TAG, "Could not set pairing confirmation due to exception: " + e); + if (JoH.ratelimit("failed pair confirmation", 200)) { + // BluetoothDevice.PAIRING_VARIANT_CONSENT) + if (type == 3) { + JoH.static_toast_long("Please confirm the bluetooth pairing request"); + return false; + } else { + JoH.static_toast_long("Failed to pair, may need to do it via Android Settings"); + device.createBond(); // for what it is worth + return false; + } + } + } + } else { + UserError.Log.e(TAG, "Received pairing request for not our device: " + device.getAddress()); + } + } else { + UserError.Log.e(TAG, "Device was null in pairing receiver"); + } + } + return true; + } + + public static String getLocalBluetoothName() { + try { + final String name = BluetoothAdapter.getDefaultAdapter().getName(); + if (name == null) return ""; + return name; + } catch (Exception e) { + return ""; + } + } + + public static boolean refreshDeviceCache(String thisTAG, BluetoothGatt gatt){ + try { + final Method method = gatt.getClass().getMethod("refresh", new Class[0]); + if (method != null) { + return (Boolean) method.invoke(gatt, new Object[0]); + } + } + catch (Exception e) { + Log.e(thisTAG, "An exception occured while refreshing gatt device cache: "+e); + } + return false; + } + + public static boolean createSpecialBond(final String thisTAG, final BluetoothDevice device){ + try { + Log.e(thisTAG,"Attempting special bond"); + Class[] argTypes = new Class[] { int.class }; + final Method method = device.getClass().getMethod("createBond", argTypes); + if (method != null) { + return (Boolean) method.invoke(device, 2); + } else { + Log.e(thisTAG,"CANNOT FIND SPECIAL BOND METHOD!!"); + } + } + catch (Exception e) { + Log.e(thisTAG, "An exception occured while creating special bond: "+e); + } + return false; + } + + + public synchronized static void setBluetoothEnabled(Context context, boolean state) { + try { + + if (isAirplaneModeEnabled(context)) { + UserError.Log.e(TAG, "Not setting bluetooth to state: " + state + " due to airplane mode being enabled"); + return; + } + + if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + + final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (bluetoothManager == null) { + UserError.Log.e(TAG, "Couldn't get bluetooth in setBluetoothEnabled"); + return; + } + BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter(); // local scope only + if (mBluetoothAdapter == null) { + UserError.Log.e(TAG, "Couldn't get bluetooth adapter in setBluetoothEnabled"); + return; + } + try { + if (state) { + UserError.Log.i(TAG, "Setting bluetooth enabled"); + mBluetoothAdapter.enable(); + } else { + UserError.Log.i(TAG, "Setting bluetooth disabled"); + mBluetoothAdapter.disable(); + + } + } catch (Exception e) { + UserError.Log.e(TAG, "Exception when enabling/disabling bluetooth: " + e); + } + } else { + UserError.Log.e(TAG, "Bluetooth low energy not supported"); + } + } finally { + // + } + } + + public static void niceRestartBluetooth(Context context) { + if (!isOngoingCall()) { + if (ratelimit("joh-restart-bluetooth", 600)) { + restartBluetooth(context); + } + } + } + + public synchronized static void restartBluetooth(final Context context) { + restartBluetooth(context, 0); + } + + public synchronized static void restartBluetooth(final Context context, final int startInMs) { + new Thread() { + @Override + public void run() { + final PowerManager.WakeLock wl = getWakeLock("restart-bluetooth", 60000); + Log.d(TAG, "Restarting bluetooth"); + try { + if (startInMs > 0) { + try { + Thread.sleep(startInMs); + } catch (InterruptedException e) { + Log.d(TAG, "Got interrupted waiting to start resetBluetooth"); + } + } + setBluetoothEnabled(context, false); + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + Log.d(TAG, "Got interrupted in resetBluetooth"); + } + setBluetoothEnabled(context, true); + } finally { + releaseWakeLock(wl); + } + } + }.start(); + } + + + public static synchronized void unBond(String transmitterMAC) { + + UserError.Log.d(TAG, "unBond() start"); + if (transmitterMAC == null) return; + try { + final BluetoothAdapter mBluetoothAdapter = ((BluetoothManager) xdrip.getAppContext().getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter(); + + final Set pairedDevices = mBluetoothAdapter.getBondedDevices(); + if (pairedDevices.size() > 0) { + for (BluetoothDevice device : pairedDevices) { + if (device.getAddress() != null) { + if (device.getAddress().equals(transmitterMAC)) { + try { + UserError.Log.e(TAG, "removingBond: " + transmitterMAC); + Method m = device.getClass().getMethod("removeBond", (Class[]) null); + m.invoke(device, (Object[]) null); + + } catch (Exception e) { + UserError.Log.e(TAG, e.getMessage(), e); + } + } + + } + } + } + } catch (Exception e) { + UserError.Log.e(TAG, "Exception during unbond! " + transmitterMAC, e); + } + UserError.Log.d(TAG, "unBond() finished"); + } + + + public static Map bundleToMap(Bundle bundle) { + final HashMap map = new HashMap<>(); + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value != null) { + map.put(key, value.toString()); + } + } + return map; + } + + public static void threadSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // + } + } + + public static ByteBuffer bArrayAsBuffer(byte[] bytes) { + final ByteBuffer bb = ByteBuffer.allocate(bytes.length); + bb.put(bytes); + return bb; + } + + public static long checksum(byte[] bytes) { + if (bytes == null) return 0; + final CRC32 crc = new CRC32(); + crc.update(bytes); + return crc.getValue(); + } + + + + public static byte[] bchecksum(byte[] bytes) { + final long c = checksum(bytes); + final byte[] buf = new byte[4]; + ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN).putInt((int) c); + return buf; + } + + public static boolean checkChecksum(byte[] bytes) { + if ((bytes == null) || (bytes.length < 4)) return false; + final CRC32 crc = new CRC32(); + crc.update(bytes, 0, bytes.length - 4); + final long buffer_crc = UnsignedInts.toLong(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt(bytes.length - 4)); + return buffer_crc == crc.getValue(); + } + + public static int parseIntWithDefault(String number, int radix, int defaultVal) { + try { + return Integer.parseInt(number, radix); + } catch (NumberFormatException e) { + Log.e(TAG, "Error parsing integer number = " + number + " radix = " + radix); + return defaultVal; + } + } + + public static double roundDouble(final double value, int places) { + if (places < 0) throw new IllegalArgumentException("Invalid decimal places"); + BigDecimal bd = new BigDecimal(value); + bd = bd.setScale(places, RoundingMode.HALF_UP); + return bd.doubleValue(); + } + + public static float roundFloat(final float value, int places) { + if (places < 0) throw new IllegalArgumentException("Invalid decimal places"); + BigDecimal bd = new BigDecimal(value); + bd = bd.setScale(places, RoundingMode.HALF_UP); + return bd.floatValue(); + } + + public static boolean isServiceRunningInForeground(Class serviceClass) { + final ActivityManager manager = (ActivityManager) xdrip.getAppContext().getSystemService(Context.ACTIVITY_SERVICE); + try { + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return service.foreground; + } + } + return false; + } catch (NullPointerException e) { + return false; + } + } + + public static boolean emptyString(final String str) { + return str == null || str.length() == 0; + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2RawValue.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2RawValue.java new file mode 100644 index 0000000..7032596 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2RawValue.java @@ -0,0 +1,62 @@ +package com.eveningoutpost.dexdrip.Models; + + + import android.provider.BaseColumns; + + import com.activeandroid.annotation.Column; + import com.activeandroid.annotation.Table; + import com.activeandroid.query.Select; + + import java.util.Date; + import java.util.List; + +@Table(name = "Libre2RawValue2", id = BaseColumns._ID) +public class Libre2RawValue extends PlusModel { + + static final String[] schema = { + "DROP TABLE Libre2RawValue;", + "CREATE TABLE Libre2RawValue2 (_id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, serial STRING, glucose REAL);", + "CREATE INDEX index_Libre2RawValue2_ts on Libre2RawValue2(ts);" + }; + + @Column(name = "serial", index = true) + public String serial; + + @Column(name = "ts", index = true) + public long timestamp; + + @Column(name = "glucose", index = false) + public double glucose; + + public static List last20Minutes() { + double timestamp = (new Date().getTime()) - (60000 * 20); + return new Select() + .from(Libre2RawValue.class) + .where("ts >= " + timestamp) + .orderBy("ts asc") + .execute(); + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + return new Select() + .from(Libre2RawValue.class) + .where("ts >= " + Math.max(startTime, 0)) + .where("ts <= " + endTime) + .where("glucose != 0") + .orderBy("ts desc") + .limit(number) + .execute(); + } + + public static void updateDB() { + fixUpTable(schema, false); + } +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2Sensor.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2Sensor.java new file mode 100644 index 0000000..3a76187 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Libre2Sensor.java @@ -0,0 +1,62 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; +import android.text.format.DateFormat; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; + +import java.sql.SQLException; +import java.util.List; + +@Table(name = "Libre2Sensors", id = BaseColumns._ID) +public class Libre2Sensor extends PlusModel { + static final String TAG = "Libre2Sensor"; + + static final String[] schema = { + "CREATE VIEW Libre2Sensors AS SELECT MIN(_id) as _id, serial, MIN(ts) as ts_from, MAX(ts) AS ts_to, COUNT(*) AS readings FROM Libre2RawValue2 GROUP BY serial ORDER BY ts DESC;" + }; + + @Column(name = "serial", index = true) + public String serial; + + @Column(name = "ts_from", index = false) + public long ts_from; + + @Column(name = "ts_to", index = false) + public long ts_to; + + @Column(name = "readings", index = false) + public long readings; + + private static volatile String cachedStringSensors = null; + + public static String Libre2Sensors() { + String Sum = ""; + + if ((cachedStringSensors == null) || (JoH.ratelimit("libre2sensor-report", 120))) { + + List rs = new Select() + .from(Libre2Sensor.class) + .execute(); + + for (Libre2Sensor Sensorpart : rs) { + Long Diff_ts = Sensorpart.ts_to - Sensorpart.ts_from; + Sum = Sum + Sensorpart.serial + + "\n" + DateFormat.format("dd.MM.yy", Sensorpart.ts_from) + + " to: " + DateFormat.format("dd.MM.yy", Sensorpart.ts_to) + + " (" + JoH.niceTimeScalarShortWithDecimalHours(Diff_ts) + ")" + + " readings: " + ((Sensorpart.readings * 100) / (Diff_ts / 60000)) + "%\n" + + "------------------\n"; + } + cachedStringSensors=Sum; + } + + return cachedStringSensors; + } + + public static void updateDB() { + fixUpTable(schema, false); + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreBlock.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreBlock.java new file mode 100644 index 0000000..1ffbcf6 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreBlock.java @@ -0,0 +1,192 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue; +import com.google.gson.annotations.Expose; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.UUID; +/** + * Created by jamorham on 19/10/2017. + */ + +@Table(name = "LibreBlock", id = BaseColumns._ID) +public class LibreBlock extends PlusModel { + + private static final String TAG = "LibreBlock"; + static final String[] schema = { + "CREATE TABLE LibreBlock (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE LibreBlock ADD COLUMN timestamp INTEGER;", + "ALTER TABLE LibreBlock ADD COLUMN reference TEXT;", + "ALTER TABLE LibreBlock ADD COLUMN blockbytes BLOB;", + "ALTER TABLE LibreBlock ADD COLUMN bytestart INTEGER;", + "ALTER TABLE LibreBlock ADD COLUMN byteend INTEGER;", + "ALTER TABLE LibreBlock ADD COLUMN calculatedbg REAL;", + "ALTER TABLE LibreBlock ADD COLUMN uuid TEXT;", + "ALTER TABLE LibreBlock ADD COLUMN patchUid BLOB;", + "ALTER TABLE LibreBlock ADD COLUMN patchInfo BLOB;", + "CREATE INDEX index_LibreBlock_timestamp on LibreBlock(timestamp);", + "CREATE INDEX index_LibreBlock_bytestart on LibreBlock(bytestart);", + "CREATE INDEX index_LibreBlock_byteend on LibreBlock(byteend);", + "CREATE INDEX index_LibreBlock_uuid on LibreBlock(uuid);" + }; + + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + @Expose + @Column(name = "bytestart", index = true) + public long byte_start; + + @Expose + @Column(name = "byteend", index = true) + public long byte_end; + + @Expose + @Column(name = "reference", index = true) + public String reference; + + @Expose + @Column(name = "blockbytes") + public byte[] blockbytes; + + @Expose + @Column(name = "calculatedbg") + public double calculated_bg; + + @Expose + @Column(name = "uuid", index = true) + public String uuid; + + @Expose + @Column(name = "patchUid") + public byte[] patchUid; + + @Expose + @Column(name = "patchInfo") + public byte[] patchInfo; + + public static LibreBlock createAndSave(String reference, long timestamp, byte[] blocks, int byte_start) { + return createAndSave(reference, timestamp, blocks, byte_start, false, null, null); + } + + // if you are indexing by block then just * 8 to get byte start + public static LibreBlock createAndSave(String reference, long timestamp, byte[] blocks, int byte_start, boolean allowUpload, byte[] patchUid, byte[] patchInfo) { + final LibreBlock lb = create(reference, timestamp, blocks, byte_start, patchUid, patchInfo); + if (lb != null) { + lb.save(); + if(byte_start == 0 && blocks.length == 344 && allowUpload) { + Log.d(TAG, "sending new item to queue"); + UploaderQueue.newTransmitterDataEntry("create" ,lb); + } + } + return lb; + } + + private static LibreBlock create(String reference, long timestamp, byte[] blocks, int byte_start, byte[] patchUid, byte[] patchInfo) { + UserError.Log.e(TAG,"Backtrack: "+JoH.backTrace()); + if (reference == null) { + UserError.Log.e(TAG, "Cannot save block with null reference"); + return null; + } + if (blocks == null) { + UserError.Log.e(TAG, "Cannot save block with null data"); + return null; + } + + final LibreBlock lb = new LibreBlock(); + lb.reference = reference; + lb.blockbytes = blocks; + lb.byte_start = byte_start; + lb.byte_end = byte_start + blocks.length; + lb.timestamp = timestamp; + lb.patchUid = patchUid; + lb.patchInfo = patchInfo; + lb.uuid = UUID.randomUUID().toString(); + return lb; + } + + public static LibreBlock getLatestForTrend() { + return getLatestForTrend(JoH.tsl() - Constants.DAY_IN_MS, JoH.tsl() ); + } + + + public static LibreBlock getLatestForTrend(long start_time, long end_time) { + + return new Select() + .from(LibreBlock.class) + .where("bytestart == 0") + .where("byteend >= 344") + .where("timestamp >= ?", start_time) + .where("timestamp <= ?", end_time) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static List getForTrend(long start_time, long end_time) { + + return new Select() + .from(LibreBlock.class) + .where("bytestart == 0") + .where("byteend >= 344") + .where("timestamp >= ?", start_time) + .where("timestamp <= ?", end_time) + .orderBy("timestamp asc") + .execute(); + } + + public static LibreBlock getForTimestamp(long timestamp) { + final long margin = (3 * 1000); + return new Select() + .from(LibreBlock.class) + .where("timestamp >= ?", (timestamp - margin)) + .where("timestamp <= ?", (timestamp + margin)) + .executeSingle(); + } + + public static void UpdateBgVal(long timestamp, double calculated_value) { + LibreBlock libreBlock = getForTimestamp(timestamp); + if (libreBlock == null) { + return; + } + Log.e(TAG, "Updating bg for timestamp " + timestamp); + libreBlock.calculated_bg = calculated_value; + libreBlock.save(); + } + + public static LibreBlock findByUuid(String uuid) { + try { + return new Select() + .from(LibreBlock.class) + .where("uuid = ?", uuid) + .executeSingle(); + } catch (Exception e) { + Log.e(TAG,"findByUuid() Got exception on Select : "+e.toString()); + return null; + } + } + + private static final boolean d = false; + + public static void updateDB() { + fixUpTable(schema, false); + } + + public static LibreBlock byid(long id) { + return new Select() + .from(LibreBlock.class) + .where("_ID = ?", id) + .executeSingle(); + } + + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreData.java new file mode 100644 index 0000000..46ee1f8 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreData.java @@ -0,0 +1,54 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.google.gson.annotations.Expose; + +/** + * Created by jamorham on 19/10/2017. + */ + +@Table(name = "LibreData", id = BaseColumns._ID) +public class LibreData extends PlusModel { + private static final String TAG = "LibreData"; + static final String[] schema = { + "CREATE TABLE LibreData (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE LibreData ADD COLUMN timestamp INTEGER;", + "ALTER TABLE LibreData ADD COLUMN temperature REAL;", + "ALTER TABLE LibreData ADD COLUMN temperatureraw INTEGER;", + + "CREATE INDEX index_LibreData_timestamp on LibreData(timestamp);" + }; + + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + @Expose + @Column(name = "temperature") + public double temperature; + + @Expose + @Column(name = "temperatureraw") + public long temperatureraw; + + + public static LibreData create(byte[] temp_bytes) { + final LibreData ld = new LibreData(); + ld.timestamp = JoH.tsl(); + // TODO + //ld.temperatureraw = get byte order value from temp_bytes + //ld.temperature = evaluate temperature from temperature raw + return ld; + } + + private static final boolean d = false; + + public static void updateDB() { + fixUpTable(schema, false); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreOOPAlgorithm.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreOOPAlgorithm.java new file mode 100644 index 0000000..3f36325 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreOOPAlgorithm.java @@ -0,0 +1,145 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Intent; +import android.os.Bundle; + +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; +import com.eveningoutpost.dexdrip.LibreAlarmReceiver; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Intents; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.ArrayList; + +public class LibreOOPAlgorithm { + private static final String TAG = "LibreOOPAlgorithm"; + + static public void SendData(byte[] fullData, long timestamp) { + SendData(fullData, timestamp, null, null); + } + + static public void SendData(byte[] fullData, long timestamp, byte []patchUid, byte []patchInfo) { + if(fullData == null) { + Log.e(TAG, "SendData called with null data"); + return; + } + + if(fullData.length < 344) { + Log.e(TAG, "SendData called with data size too small. " + fullData.length); + return; + } + Log.i(TAG, "Sending full data to OOP Algorithm data-len = " + fullData.length); + + fullData = java.util.Arrays.copyOfRange(fullData, 0, 0x158); + Log.i(TAG, "Data that will be sent is " + HexDump.dumpHexString(fullData)); + + Intent intent = new Intent(Intents.XDRIP_PLUS_LIBRE_DATA); + Bundle bundle = new Bundle(); + bundle.putByteArray(Intents.LIBRE_DATA_BUFFER, fullData); + bundle.putLong(Intents.LIBRE_DATA_TIMESTAMP, timestamp); + bundle.putString(Intents.LIBRE_SN, PersistentStore.getString("LibreSN")); + bundle.putInt(Intents.LIBRE_RAW_ID, android.os.Process.myPid()); + + if(patchUid != null) { + bundle.putByteArray(Intents.LIBRE_PATCH_UID_BUFFER, patchUid); + } + if(patchInfo != null) { + bundle.putByteArray(Intents.LIBRE_PATCH_INFO_BUFFER, patchInfo); + } + + intent.putExtras(bundle); + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + + final String packages = PersistentStore.getString(CompatibleApps.EXTERNAL_ALG_PACKAGES); + if (packages.length() > 0) { + final String[] packagesE = packages.split(","); + for (final String destination : packagesE) { + if (destination.length() > 3) { + intent.setPackage(destination); + Log.d(TAG, "Sending to package: " + destination); + xdrip.getAppContext().sendBroadcast(intent); + } + } + } else { + Log.d(TAG, "Sending to generic package"); + xdrip.getAppContext().sendBroadcast(intent); + } + } + + + static public void HandleData(String oopData) { + Log.e(TAG, "HandleData called with " + oopData); + OOPResults oOPResults = null; + try { + final Gson gson = new GsonBuilder().create(); + OOPResultsContainer oOPResultsContainer = gson.fromJson(oopData, OOPResultsContainer.class); + + if(oOPResultsContainer.Message != null) { + Log.e(TAG, "recieved a message from oop algorithm:" + oOPResultsContainer.Message); + } + + if(oOPResultsContainer.oOPResultsArray.length > 0) { + oOPResults = oOPResultsContainer.oOPResultsArray[0]; + } else { + Log.e(TAG, "oOPResultsArray exists, but size is zero"); + return; + } + } catch (Exception e) { //TODO: what exception should we catch here. + Log.e(TAG, "HandleData cought exception ", e); + return; + } + boolean use_raw = Pref.getBooleanDefaultFalse("calibrate_external_libre_algorithm"); + ReadingData.TransferObject libreAlarmObject = new ReadingData.TransferObject(); + libreAlarmObject.data = new ReadingData(); + libreAlarmObject.data.trend = new ArrayList(); + + double factor = 1; + if(use_raw) { + // When handeling raw, data is expected to be bigger in a factor of 1000 and + // is then devided by Constants.LIBRE_MULTIPLIER + factor = 1000 / Constants.LIBRE_MULTIPLIER; + } + + // Add the first object, that is the current time + GlucoseData glucoseData = new GlucoseData(); + glucoseData.sensorTime = oOPResults.currentTime; + glucoseData.realDate = oOPResults.timestamp; + glucoseData.glucoseLevel = (int)(oOPResults.currentBg * factor); + glucoseData.glucoseLevelRaw = (int)(oOPResults.currentBg * factor); + + libreAlarmObject.data.trend.add(glucoseData); + + // TODO: Add here data of last 10 minutes or whatever. + + + // Add the historic data + libreAlarmObject.data.history = new ArrayList(); + for(HistoricBg historicBg : oOPResults.historicBg) { + if(historicBg.quality == 0) { + glucoseData = new GlucoseData(); + glucoseData.realDate = oOPResults.timestamp + (historicBg.time - oOPResults.currentTime) * 60000; + glucoseData.glucoseLevel = (int)(historicBg.bg * factor); + glucoseData.glucoseLevelRaw = (int)(historicBg.bg * factor); + libreAlarmObject.data.history.add(glucoseData); + } + } + + // Add the current point again. This is needed in order to have the last gaps closed. + // TODO: Base this on real BG values. + glucoseData = new GlucoseData(); + glucoseData.realDate = oOPResults.timestamp; + glucoseData.glucoseLevel = (int)(oOPResults.currentBg * factor); + glucoseData.glucoseLevelRaw = (int)(oOPResults.currentBg * factor); + libreAlarmObject.data.history.add(glucoseData); + + Log.e(TAG, "HandleData Created the following object " + libreAlarmObject.toString()); + LibreAlarmReceiver.CalculateFromDataTransferObject(libreAlarmObject, use_raw); + + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/NSClientChat.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/NSClientChat.java new file mode 100644 index 0000000..0749bc5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/NSClientChat.java @@ -0,0 +1,83 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.util.Log; + +import com.eveningoutpost.dexdrip.UtilityModels.Intents; +import com.eveningoutpost.dexdrip.xdrip; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +/** + * Created by jamorham on 23/02/2016. + */ +public class NSClientChat { + + private static final String TAG = "jamorham nsclient"; + + + public static void pushTreatmentAsync(final Treatments thistreatment) { + Thread testAddTreatment = new Thread() { + @Override + public void run() { + + try { + Context context = xdrip.getAppContext(); + JSONObject data = new JSONObject(); + if (thistreatment.carbs > 0) { + if (thistreatment.insulin > 0) { + data.put("eventType", "Meal Bolus"); + } else { + data.put("eventType", "Carb Correction"); + } + } else { + if (thistreatment.insulin > 0) { + data.put("eventType", "Correction Bolus"); + } else { + if ((thistreatment.notes != null) && (thistreatment.notes.length() > 1)) { + data.put("eventType", "Note"); + } else { + data.put("eventType", ""); + } + } + } + + data.put("insulin", thistreatment.insulin); + if (thistreatment.insulinJSON != null) { + data.put("insulinInjections", thistreatment.insulinJSON); + } + data.put("carbs", thistreatment.carbs); + if (thistreatment.notes != null) { + data.put("notes", thistreatment.notes); + } + // data.put("_id", thistreatment.uuid.replace("-","")); + //data.put("uuid",thistreatment.uuid); + data.put("created_at", DateUtil.toISOString(thistreatment.timestamp)); + // data.put("NSCLIENTTESTRECORD", "NSCLIENTTESTRECORD"); + Bundle bundle = new Bundle(); + bundle.putString("action", "dbAdd"); + bundle.putString("collection", "treatments"); // "treatments" || "entries" || "devicestatus" || "profile" || "food" + bundle.putString("data", data.toString()); + Intent intent = new Intent(Intents.ACTION_DATABASE); + intent.putExtras(bundle); + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + context.sendBroadcast(intent); + List q = context.getPackageManager().queryBroadcastReceivers(intent, 0); + if (q.size() < 1) { + Log.e(TAG, "DBADD No receivers"); + } else Log.e(TAG, "DBADD dbAdd " + q.size() + " receivers"); + } catch (JSONException e) { + Log.e(TAG, "Got exception with parsing: " + e.toString()); + } + } + }; + testAddTreatment.start(); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Noise.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Noise.java new file mode 100644 index 0000000..38338fd --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Noise.java @@ -0,0 +1,26 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.eveningoutpost.dexdrip.UtilityModels.Pref; + +/** + * Created by jamorham on 04/03/2018. + */ + +// TODO future move noise trigger constants here + +public class Noise { + + private static final String TAG = "xDripNoise"; + + public static int getNoiseBlockLevel() { + int value = 200; + try { + value = Integer.parseInt(Pref.getString("noise_block_level", "200")); + } catch (NumberFormatException e) { + UserError.Log.e(TAG, "Cannot process noise block level: " + e); + } + return value; + + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/OOPResultsContainer.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/OOPResultsContainer.java new file mode 100644 index 0000000..6254727 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/OOPResultsContainer.java @@ -0,0 +1,45 @@ +package com.eveningoutpost.dexdrip.Models; + + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +class HistoricBg { + + public int quality; + public int time; + public double bg; +} + +class OOPResults { + double currentBg; + int currentTime; + int currentTrend; + HistoricBg [] historicBg; + long timestamp; + String serialNumber; + + String toGson() { + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); + } +} + +public class OOPResultsContainer { + OOPResultsContainer() { + oOPResultsArray = new OOPResults[0]; + version = 1; + } + + String toGson() { + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); + } + + int version; + String Message; + + OOPResults[] oOPResultsArray; +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/PenData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PenData.java new file mode 100644 index 0000000..c5611f5 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PenData.java @@ -0,0 +1,159 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.google.gson.annotations.Expose; + +import java.util.List; +import java.util.UUID; + +@Table(name = "PenData", id = BaseColumns._ID) +public class PenData extends Model { + + private static final String TAG = "PenData"; + + @Expose + @Column(name = "pen_mac", index = true) + public String mac; + + @Expose + @Column(name = "typ", index = true) + public String type; + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "idx") + public long index; + + @Expose + @Column(name = "created_timestamp") + public long created_timestamp; + + @Expose + @Column(name = "units") + public double units; + + @Expose + @Column(name = "temperature") + public double temperature; + + @Expose + @Column(name = "battery") + public int battery; + + @Expose + @Column(name = "flags") + public long flags; + + @Expose + @Column(name = "bitmap") + public long bitmap_flags; + + @Expose + @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public String uuid; + + @Expose + @Column(name = "insulin_name") + public String insulin_name; + + + @Expose + @Column(name = "raw") + public byte[] raw; + + private static final String[] schema = { + "CREATE TABLE PenData (_id INTEGER PRIMARY KEY AUTOINCREMENT, battery INTEGER, created_timestamp INTEGER, flags INTEGER, idx INTEGER, pen_mac TEXT, raw BLOB, temperature REAL, timestamp INTEGER UNIQUE ON CONFLICT FAIL, typ TEXT, units REAL, uuid TEXT UNIQUE ON CONFLICT FAIL);", + "ALTER TABLE PenData ADD COLUMN bitmap INTEGER;", + "ALTER TABLE PenData ADD COLUMN insulin_name TEXT;", + "CREATE INDEX index_PenData_pen_mac on PenData(pen_mac);", + "CREATE INDEX index_PenData_timestamp on PenData(timestamp);", + "CREATE INDEX index_PenData_typ on PenData(typ);", + "CREATE UNIQUE INDEX index_unq on PenData(pen_mac,idx);" + }; + + public static void updateDB() { + PlusModel.fixUpTable(schema, false); + } + + + public static PenData create(final String mac, final String type, final int index, final double units, final long timestamp, final double temperature, final byte[] raw) { + + // TODO baulk on very old records + + if (mac == null || type == null || index < 0 || units == -1 || timestamp < 0) { + UserError.Log.wtf(TAG, "Invalid data sent to PenData.create() - skipping"); + return null; + } + + // NOTE negative units indicates rewind + final PenData penData = new PenData(); + penData.created_timestamp = JoH.tsl(); + penData.uuid = UUID.randomUUID().toString(); + penData.index = index; + penData.units = units; + penData.timestamp = timestamp; + penData.temperature = temperature; + penData.mac = mac; + penData.type = type; + penData.raw = raw; + return penData; + } + + public static long getHighestIndex(final String mac) { + if (mac == null) return -1; + final PenData penData = new Select() + .from(PenData.class) + .where("pen_mac = ?", mac) + .orderBy("idx desc") + .executeSingle(); + return penData != null ? penData.index : -1; + } + + + public static long getMissingIndex(final String mac) { + if (mac == null) return -1; + final List list = new Select() + .from(PenData.class) + .where("pen_mac = ?", mac) + .where("timestamp > ?", JoH.tsl() - Constants.WEEK_IN_MS) + .orderBy("idx desc") + .execute(); + long got = -1; + for (final PenData pd : list) { + if (got != -1 && pd.index != got - 1) { + UserError.Log.d(TAG, "Tripped missing index on: " + got + " vs " + pd.index); + return got - 1; + } + got = pd.index; + } + return -1; + } + + public static List getAllRecordsBetween(final long start, final long end) { + final List list = new Select() + .from(PenData.class) + .where("timestamp >= ?", start) + .where("timestamp <= ?", end) + .orderBy("typ asc, pen_mac asc, timestamp asc") + .execute(); + return list; + } + + + public String brief() { + return mac + " " + JoH.dateTimeText(timestamp) + " " + units + "U"; + } + + public String penName() { + return type + " " + mac; // TODO have some way to name pen better + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/PlusModel.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PlusModel.java new file mode 100644 index 0000000..8c782d7 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PlusModel.java @@ -0,0 +1,25 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.activeandroid.Model; +import com.activeandroid.util.SQLiteUtils; + +/** + * Created by jamorham on 01/02/2017. + */ + +public class PlusModel extends Model { + + protected synchronized static boolean fixUpTable(String[] schema, boolean patched) { + if (patched) return true; + + for (String patch : schema) { + try { + SQLiteUtils.execSql(patch); + } catch (Exception e) { + // + } + } + return true; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Prediction.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Prediction.java new file mode 100644 index 0000000..5bb639a --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Prediction.java @@ -0,0 +1,132 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +/** + * Created by jamorham on 11/06/2018. + */ + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "Prediction", id = BaseColumns._ID) +public class Prediction extends PlusModel { + + private static boolean patched = false; + private final static String TAG = Prediction.class.getSimpleName(); + private final static boolean d = false; + + private static final String[] schema = { + "CREATE TABLE Prediction (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE Prediction ADD COLUMN timestamp INTEGER;", + "ALTER TABLE Prediction ADD COLUMN glucose REAL;", + "ALTER TABLE Prediction ADD COLUMN source TEXT;", + "ALTER TABLE Prediction ADD COLUMN note TEXT;", + "CREATE INDEX index_Prediction_source on Prediction(source);", + "CREATE UNIQUE INDEX index_Prediction_timestamp on Prediction(timestamp);"}; + + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "glucose") + public double glucose; + + @Expose + @Column(name = "source") + public String source; + + @Expose + @Column(name = "note") + public String note; + + + public static Prediction create(long timestamp, int glucose, String source) { + final Prediction prediction = new Prediction(); + prediction.timestamp = timestamp; + prediction.glucose = glucose; + prediction.source = source; + return prediction; + } + + public Prediction addNote(String note) { + this.note = note; + return this; + } + + + public String toS() { + return JoH.defaultGsonInstance().toJson(this); + } + + // static methods + + public static Prediction last() { + try { + return new Select() + .from(Prediction.class) + .orderBy("timestamp desc") + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + updateDB(); + return null; + } + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + final List results = new Select() + .from(Prediction.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + + return results; + } catch (android.database.sqlite.SQLiteException e) { + updateDB(); + return new ArrayList<>(); + } + } + + + public static List cleanup(int retention_days) { + return new Delete() + .from(Prediction.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L)) + .execute(); + } + + + // create the table ourselves without worrying about model versioning and downgrading + public static void updateDB() { + patched = fixUpTable(schema, patched); + } +} + + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/PredictionData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PredictionData.java new file mode 100644 index 0000000..048f546 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/PredictionData.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.Models; + +// class from LibreAlarm + +public class PredictionData extends GlucoseData { + + public enum Result { + OK, + ERROR_NO_NFC, + ERROR_NFC_READ + } + + public double trend = -1; + public double confidence = -1; + public Result errorCode; + public int attempt; + + public PredictionData() {} + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/ProcessInitialDataQuality.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ProcessInitialDataQuality.java new file mode 100644 index 0000000..dae6a18 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ProcessInitialDataQuality.java @@ -0,0 +1,124 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.databinding.BaseObservable; + +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.eveningoutpost.dexdrip.xdrip; + +import java.util.List; + +import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.STALE_CALIBRATION_CUT_OFF; + +/** + * Created by jamorham on 01/10/2017. + *

+ * Provides InitialDataQuality status object used to determine + * whether we can process an initial calibration right now + *

+ * Currently does not handle interpolation or interstitial lag + */ + +public class ProcessInitialDataQuality { + + public static InitialDataQuality getInitialDataQuality() { + // get uncalculated data + JoH.clearCache(); + final List uncalculated = BgReading.latestUnCalculated(3); + return getInitialDataQuality(uncalculated); + } + + // Check if data looks suitable for initial calibration + // List must be supplied with newest data first and without duplicates + public static InitialDataQuality getInitialDataQuality(final List uncalculated) { + final InitialDataQuality result = new InitialDataQuality(); + String alert = ""; + final Boolean service_running = DexCollectionType.getServiceRunningState(); + if (service_running != null) result.collector_running = service_running; + + // if there is no data at all within calibration cut off then return negative result + if (!((uncalculated == null) || (uncalculated.size() < 1))) { + // we have at least one record + result.received_raw_data = true; + result.last_activity = uncalculated.get(0).timestamp; + + // work out next likely time to receive a reading + final long OUR_PERIOD = DEXCOM_PERIOD; // eventually to come from DexCollectionType + result.next_activity_expected = result.last_activity + OUR_PERIOD; + // if time already past based on last timestamp then work out in to the future based on period + if (JoH.tsl() > result.next_activity_expected) { + result.missed_last = true; + final long offset = result.last_activity % OUR_PERIOD; + result.next_activity_expected = ((JoH.tsl() / OUR_PERIOD) * OUR_PERIOD) + offset; + + // this logic could probably be improved + while (result.next_activity_expected < JoH.tsl()) { + result.next_activity_expected += OUR_PERIOD; + } + + } + if (!(JoH.msSince(uncalculated.get(0).timestamp) > STALE_CALIBRATION_CUT_OFF)) { + // we got some data now see if it is recent enough + result.recent_data = true; + boolean adjusted = true; // run one time + while (adjusted) { + adjusted = false; + for (int i = 0; i < uncalculated.size(); i++) { + if (JoH.msSince(uncalculated.get(i).timestamp) > STALE_CALIBRATION_CUT_OFF) { + uncalculated.remove(i); + adjusted = true; + break; + } + if (!SensorSanity.isRawValueSane(uncalculated.get(i).raw_data)) { + uncalculated.remove(i); + adjusted = true; + alert = " "+"Raw Sensor data is outside valid range! Sensor problem!"; + break; + } + + } + } + result.number_of_records_inside_window = uncalculated.size(); + // do we have enough good data? + if (uncalculated.size() >= 3) { + if (JoH.msSince(uncalculated.get(2).timestamp) > STALE_CALIBRATION_CUT_OFF) { + result.advice = "Oldest of last 3 readings is more than " + JoH.niceTimeScalar(STALE_CALIBRATION_CUT_OFF) + " ago" + alert; + } else { + result.advice = "Readings look suitable for calibration" + alert; + result.pass = true; + } + } else { + result.advice = "Need 3 recent readings, got only " + uncalculated.size() + " so far" + alert; + } + } else { + result.advice = "No data received in last " + JoH.niceTimeScalar(STALE_CALIBRATION_CUT_OFF) + alert; + } + } else { + result.advice = "No data received yet" + alert; + } + return result; + } + + private static String gs(int id) { + return xdrip.getAppContext().getString(id); + } + + + public static class InitialDataQuality extends BaseObservable { + public boolean collector_running = false; + public boolean received_raw_data = false; + public boolean recent_data = false; + public boolean pass = false; + public boolean missed_last = false; + public int number_of_records_inside_window = 0; + public long last_activity = 0; + public long next_activity_expected = 0; + public String advice = ""; + + public String getNextExpectedTill() { + return JoH.niceTimeTill(next_activity_expected); + } + + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Profile.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Profile.java new file mode 100644 index 0000000..79c8136 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Profile.java @@ -0,0 +1,250 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.profileeditor.ProfileEditor; +import com.eveningoutpost.dexdrip.profileeditor.ProfileItem; +import com.eveningoutpost.dexdrip.xdrip; + +import java.util.List; + +/** + * Created by jamorham on 04/01/16. + */ + +// user profile for insulin related parameters which can be configured and set at times of day +// currently a placeholder with hardcoded values + +// TODO Proper support for MG/DL + + +public class Profile { + + private final static String TAG = "jamorham pred"; + public static double minimum_shown_iob = 0.005; + public static double minimum_shown_cob = 0.01; + public static double minimum_insulin_recommendation = 0.1; + public static double minimum_carb_recommendation = 1; + public static double scale_factor = 18; + private static double the_carb_ratio = 10; // now defunct + private static double stored_default_sensitivity = 54; // now defunct + private static double stored_default_absorption_rate = 35; + private static double stored_default_insulin_action_time = 3.0; + private static double stored_default_carb_delay_minutes = 15; + private static boolean preferences_loaded = false; + private static List profileItemList; + + public static double getSensitivity(double when) { + final double sensitivity = findItemListElementForTime(when).sensitivity; + // Log.d(TAG,"sensitivity: "+sensitivity); + return sensitivity; + // expressed in native units lowering effect of 1U + } + + public static void setSensitivityDefault(double value) { + // sanity check goes here + stored_default_sensitivity = value; + } + + public static void setInsulinActionTimeDefault(double value) { + // sanity check goes here + if (value < 0.1) return; + if (value > 24) return; + stored_default_insulin_action_time = value; + } + + static double getCarbAbsorptionRate(double when) { + return stored_default_absorption_rate; // carbs per hour + } + + public static void setCarbAbsorptionDefault(double value) { + // sanity check goes here + if (value < 0.01) return; + stored_default_absorption_rate = value; + } + + static double insulinActionTime(double when) { + return stored_default_insulin_action_time; + } + + static double carbDelayMinutes(double when) { + return stored_default_carb_delay_minutes; + } + + static double maxLiverImpactRatio(double when) { + return 0.3; // how much can the liver block carbs going in to blood stream? + } + + public static double getCarbRatio(double when) { + return findItemListElementForTime(when).carb_ratio; + //return the_carb_ratio; // g per unit + } + + + private static void populateProfile() { + if (profileItemList == null) { + profileItemList = ProfileEditor.loadData(false); + Log.d(TAG, "Loaded profile data, blocks: " + profileItemList.size()); + } + } + + public static void invalidateProfile() { + profileItemList = null; + } + + private static ProfileItem findItemListElementForTime(double when) { + populateProfile(); + // TODO does this want/need a hash table lookup cache? + if (profileItemList.size() == 1) + profileItemList.get(0); // always will be first/only element. + // get time of day + final int min = ProfileItem.timeStampToMin(when); + // find element + for (ProfileItem item : profileItemList) { + if (item.start_min < item.end_min) { + // regular + if ((item.start_min <= min) && (item.end_min >= min)) { + // Log.d(TAG, "Match on item " + item.getTimePeriod() + " " + profileItemList.indexOf(item)); + return item; + } + } else { + // item spans midnight + if ((min >= item.start_min) || (min <= item.end_min)) { + // Log.d(TAG, "midnight span Match on item " + item.getTimePeriod() + " " + profileItemList.indexOf(item)); + return item; + } + } + } + Log.wtf(TAG, "Null return from findItemListElementForTime"); + return null; // should be impossible + } + + + static public void setDefaultCarbRatio(Double value) { + if (value <= 0) { + Log.e(TAG, "Invalid default carb ratio: " + value); + return; + } + the_carb_ratio = value; // g per unit + } + + static double getLiverSensRatio(double when) { + return 2.0; + } + + public static void validateTargetRange() { + final double default_target_glucose = tolerantParseDouble(Pref.getString("plus_target_range", Double.toString(5.5 / scale_factor))); + if (default_target_glucose > tolerantParseDouble(Pref.getString("highValue", Double.toString(5.5 / scale_factor)))) { + Pref.setString("plus_target_range", JoH.qs(default_target_glucose * Constants.MGDL_TO_MMOLL, 1)); + UserError.Log.i(TAG, "Converted initial value of target glucose to mmol"); + } + } + + static double getTargetRangeInMmol(double when) { + // return tolerantParseDouble(Home.getString("plus_target_range",Double.toString(5.5 / scale_factor))); + return getTargetRangeInUnits(when) / scale_factor; + } + + public static double getTargetRangeInUnits(double when) { + return tolerantParseDouble(Pref.getString("plus_target_range", Double.toString(5.5 / scale_factor))); + //return getTargetRangeInMmol(when) * scale_factor; // TODO deal with rounding errors here? (3 decimal places?) + } + + static double getCarbSensitivity(double when) { + return getCarbRatio(when) / getSensitivity(when); + } + + static double getCarbsToRaiseByMmol(double mmol, double when) { + + double result = getCarbSensitivity(when) * mmol; + return result; + } + + static double getInsulinToLowerByMmol(double mmol, double when) { + return mmol / getSensitivity(when); + } + + // take an average of carb suggestions when our scope is between two times + static double getCarbsToRaiseByMmolBetweenTwoTimes(double mmol, double whennow, double whenthen) { + double result = (getCarbsToRaiseByMmol(mmol, whennow) + getCarbsToRaiseByMmol(mmol, whenthen)) / 2; + UserError.Log.d(TAG, "GetCarbsToRaiseByMmolBetweenTwoTimes: " + JoH.qs(mmol) + " result: " + JoH.qs(result)); + return result; + } + + static double getInsulinToLowerByMmolBetweenTwoTimes(double mmol, double whennow, double whenthen) { + return (getInsulinToLowerByMmol(mmol, whennow) + getInsulinToLowerByMmol(mmol, whenthen)) / 2; + } + + public static double[] evaluateEndGameMmol(double mmol, double endGameTime, double timeNow) { + double addcarbs = 0; + double addinsulin = 0; + final double target_mmol = getTargetRangeInMmol(endGameTime) * scale_factor; + double diff_mmol = target_mmol - mmol; + if (diff_mmol > 0) { + addcarbs = getCarbsToRaiseByMmolBetweenTwoTimes(diff_mmol, timeNow, endGameTime); + } else if (diff_mmol < 0) { + addinsulin = getInsulinToLowerByMmolBetweenTwoTimes(diff_mmol * -1, timeNow, endGameTime); + } + return new double[]{addcarbs, addinsulin}; + } + + public static void reloadPreferencesIfNeeded(SharedPreferences prefs) { + if (!preferences_loaded) reloadPreferences(prefs); + } + + public static void reloadPreferences() { + Log.d(TAG, "Reloaded profile preferences"); + reloadPreferences(PreferenceManager.getDefaultSharedPreferences(xdrip.getAppContext())); + } + + public static synchronized void reloadPreferences(SharedPreferences prefs) { + validateTargetRange(); + try { + Profile.setSensitivityDefault(tolerantParseDouble(prefs.getString("profile_insulin_sensitivity_default", "0"))); + } catch (Exception e) { + if (JoH.ratelimit("invalid-insulin-profile", 60)) { + Home.toaststatic("Invalid insulin sensitivity"); + } + } + try { + Profile.setDefaultCarbRatio(tolerantParseDouble(prefs.getString("profile_carb_ratio_default", "0"))); + } catch (Exception e) { + if (JoH.ratelimit("invalid-insulin-profile", 60)) { + Home.toaststatic("Invalid default carb ratio!"); + } + } + try { + Profile.setCarbAbsorptionDefault(tolerantParseDouble(prefs.getString("profile_carb_absorption_default", "0"))); + } catch (Exception e) { + if (JoH.ratelimit("invalid-insulin-profile", 60)) { + Home.toaststatic("Invalid carb absorption rate"); + } + } + try { + Profile.setInsulinActionTimeDefault(tolerantParseDouble(prefs.getString("xplus_insulin_dia", "3.0"))); + } catch (Exception e) { + if (JoH.ratelimit("invalid-insulin-profile", 60)) { + Home.toaststatic("Invalid insulin action time"); + } + } + profileItemList = null; + populateProfile(); + preferences_loaded = true; + } + + private static double tolerantParseDouble(String str) throws NumberFormatException { + return Double.parseDouble(str.replace(",", ".")); + + } + + // TODO placeholder + public static double getBasalRate(final long when) { + return 0d; + } + +} \ No newline at end of file diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/ReadingData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ReadingData.java new file mode 100644 index 0000000..95bf23e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/ReadingData.java @@ -0,0 +1,111 @@ +package com.eveningoutpost.dexdrip.Models; + +// class from LibreAlarm + +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import java.util.ArrayList; +import java.util.List; + +public class ReadingData { + + public PredictionData prediction; + public List trend; + public List history; + public byte[] raw_data; + + public ReadingData(PredictionData.Result result) { + this.prediction = new PredictionData(); + this.prediction.realDate = System.currentTimeMillis(); + this.prediction.errorCode = result; + this.trend = new ArrayList<>(); + this.history = new ArrayList<>(); + // The two bytes are needed here since some components don't like a null pointer. + this.raw_data = new byte[2]; + } + + public ReadingData(PredictionData prediction, List trend, List history) { + this.prediction = prediction; + this.trend = trend; + this.history = history; + } + + public ReadingData() {} + + public static class TransferObject { + public long id; + public ReadingData data; + + public TransferObject() {} + + public TransferObject(long id, ReadingData data) { + this.id = id; + this.data = data; + } + } + + // A function to calculate the smoothing based only on 3 points. + private void CalculateSmothedData3Points() { + for (int i=0; i < trend.size() - 2 ; i++) { + trend.get(i).glucoseLevelRawSmoothed = + (trend.get(i).glucoseLevelRaw + trend.get(i+1).glucoseLevelRaw + trend.get(i+2).glucoseLevelRaw) / 3; + } + // Set the last two points. (doing our best - this will only be used if there are no previous readings). + if(trend.size() >= 2) { + // We have two points, use their average for both + int average = (trend.get(trend.size()-2).glucoseLevelRaw + trend.get(trend.size()-1).glucoseLevelRaw ) / 2; + trend.get(trend.size()-2).glucoseLevelRawSmoothed = average; + trend.get(trend.size()-1).glucoseLevelRawSmoothed = average; + } else if(trend.size() == 1){ + // Only one point, use it + trend.get(trend.size()-1).glucoseLevelRawSmoothed = trend.get(trend.size()-1).glucoseLevelRaw; + } + + } + + private void CalculateSmothedData5Points() { + // In all places in the code, there should be exactly 16 points. + // Since that might change, and I'm doing an average of 5, then in the case of less then 5 points, + // I'll only copy the data as is (to make sure there are reasonable values when the function returns). + if(trend.size() < 5) { + for (int i=0; i < trend.size() - 4 ; i++) { + trend.get(i).glucoseLevelRawSmoothed = trend.get(i).glucoseLevelRaw; + } + return; + } + + for (int i=0; i < trend.size() - 4 ; i++) { + trend.get(i).glucoseLevelRawSmoothed = + (trend.get(i).glucoseLevelRaw + + trend.get(i+1).glucoseLevelRaw + + trend.get(i+2).glucoseLevelRaw + + trend.get(i+3).glucoseLevelRaw + + trend.get(i+4).glucoseLevelRaw ) / 5; + } + // We now have to calculate the last 4 points, will do our best... + trend.get(trend.size()-4).glucoseLevelRawSmoothed = + (trend.get(trend.size()-4).glucoseLevelRaw + + trend.get(trend.size()-3).glucoseLevelRaw + + trend.get(trend.size()-2).glucoseLevelRaw + + trend.get(trend.size()-1).glucoseLevelRaw ) / 4; + + trend.get(trend.size()-3).glucoseLevelRawSmoothed = + (trend.get(trend.size()-3).glucoseLevelRaw + + trend.get(trend.size()-2).glucoseLevelRaw + + trend.get(trend.size()-1).glucoseLevelRaw ) / 3; + + // Use the last two points for both last points + trend.get(trend.size()-2).glucoseLevelRawSmoothed = + (trend.get(trend.size()-2).glucoseLevelRaw + + trend.get(trend.size()-1).glucoseLevelRaw ) / 2; + + trend.get(trend.size()-1).glucoseLevelRawSmoothed = trend.get(trend.size()-2).glucoseLevelRawSmoothed; + } + + public void CalculateSmothedData() { + CalculateSmothedData5Points(); + // print the values, remove before release + for (int i=0; i < trend.size() ; i++) { + Log.e("xxx","" + i + " raw val " + trend.get(i).glucoseLevelRaw + " smoothed " + trend.get(i).glucoseLevelRawSmoothed); + } + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Reminder.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Reminder.java new file mode 100644 index 0000000..244db4d --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Reminder.java @@ -0,0 +1,399 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Context; +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.Reminders; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.utils.HomeWifi; +import com.google.gson.annotations.Expose; + +import java.util.Calendar; +import java.util.List; + + +/** + * Created by jamorham on 01/02/2017. + */ + +@Table(name = "Reminder", id = BaseColumns._ID) +public class Reminder extends Model { + + + private static final String TAG = "Reminder"; + private static boolean patched = false; + public static final String REMINDERS_ALL_DISABLED = "reminders-all-disabled"; + public static final String REMINDERS_NIGHT_DISABLED = "reminders-at-night-disabled"; + public static final String REMINDERS_RESTART_TOMORROW = "reminders-restart-tomorrow"; + public static final String REMINDERS_ADVANCED_MODE = "reminders-advanced-mode"; + private static final String[] schema = { + "CREATE TABLE Reminder (_id INTEGER PRIMARY KEY AUTOINCREMENT)", + "ALTER TABLE Reminder ADD COLUMN next_due INTEGER", + "ALTER TABLE Reminder ADD COLUMN period INTEGER", + "ALTER TABLE Reminder ADD COLUMN snoozed_till INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN last_snoozed_for INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN last_fired INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN fired_times INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN alerted_times INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN title TEXT", + "ALTER TABLE Reminder ADD COLUMN alt_title TEXT", + "ALTER TABLE Reminder ADD COLUMN sound_uri TEXT", + "ALTER TABLE Reminder ADD COLUMN ideal_time TEXT", + "ALTER TABLE Reminder ADD COLUMN priority INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN enabled INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN weekdays INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN weekends INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN repeating INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN alternating INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN alternate INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN chime INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN homeonly INTEGER DEFAULT 0", + "ALTER TABLE Reminder ADD COLUMN speak INTEGER DEFAULT 0", + "CREATE INDEX index_Reminder_next_due on Reminder(next_due)", + "CREATE INDEX index_Reminder_enabled on Reminder(enabled)", + "CREATE INDEX index_Reminder_weekdays on Reminder(weekdays)", + "CREATE INDEX index_Reminder_homeonly on Reminder(homeonly)", + "CREATE INDEX index_Reminder_weekends on Reminder(weekends)", + "CREATE INDEX index_Reminder_priority on Reminder(priority)", + "CREATE INDEX index_Reminder_timestamp on Reminder(timestamp)", + "CREATE INDEX index_Reminder_snoozed_till on Reminder(snoozed_till)" + }; + + @Expose + @Column(name = "title") + public String title; + + @Expose + @Column(name = "alt_title") + public String alternate_title; + + @Expose + @Column(name = "next_due", index = true) + public long next_due; + + @Expose + @Column(name = "period") + public long period; + + @Expose + @Column(name = "sound_uri") + public String sound_uri; + + @Expose + @Column(name = "enabled", index = true) + public boolean enabled; + + @Expose + @Column(name = "weekdays", index = true) + public boolean weekdays; + + @Expose + @Column(name = "weekends", index = true) + public boolean weekends; + + @Expose + @Column(name = "homeonly", index = true) + public boolean homeonly; + + @Expose + @Column(name = "speak") + public boolean speak; + + @Expose + @Column(name = "repeating") + public boolean repeating; + + @Expose + @Column(name = "chime") + public boolean chime_only; + + @Expose + @Column(name = "alternating") + public boolean alternating; + + @Expose + @Column(name = "alternate") + public boolean alternate; + + @Expose + @Column(name = "snoozed_till", index = true) + public long snoozed_till; + + @Expose + @Column(name = "last_fired", index = true) + public long last_fired; + + @Expose + @Column(name = "last_snoozed_for") + public long last_snoozed_for; + + @Expose + @Column(name = "fired_times") + public long fired_times; + + @Expose + @Column(name = "alerted_times") + public long alerted_times; + + @Expose + @Column(name = "priority") + public long priority; + + @Expose + @Column(name = "ideal_time") + public String ideal_time; + + + public boolean isAlternate() { + if (alternating && (alternate_title != null)) { + return alternate; + } else { + return false; + } + } + + public String getTitle() { + return isAlternate() ? alternate_title : title; + } + + public String getAlternateTitle() { + return alternate_title != null ? alternate_title : title + " alternate"; + } + + public void updateTitle(String new_title) { + if (isAlternate()) { + alternate_title = new_title; + } else { + title = new_title; + } + save(); + } + + public boolean isHoursPeriod() { + return (period >= Constants.HOUR_IN_MS) && (period < Constants.DAY_IN_MS); + } + + public boolean isDaysPeriod() { + return (period >= Constants.DAY_IN_MS) && (period < (Constants.WEEK_IN_MS * 2)); + } + + public boolean isWeeksPeriod() { + return (period >= (2 * Constants.WEEK_IN_MS)); + } + + public long periodInUnits() { + if (isDaysPeriod()) return period / Constants.DAY_IN_MS; + if (isHoursPeriod()) return period / Constants.HOUR_IN_MS; + if (isWeeksPeriod()) return period / Constants.WEEK_IN_MS; + return -1; // ERROR + } + + public boolean isDue() { + if ((enabled) && (next_due <= JoH.tsl())) { + return true; + } else { + return false; + } + } + + public boolean isSnoozed() { + if (snoozed_till > JoH.tsl()) { + return true; + } else { + return false; + } + } + + public boolean shouldNotify() { + return enabled && isDue() && !isSnoozed(); + } + + public synchronized void notified() { + if (last_fired < next_due) fired_times++; + alerted_times++; + last_fired = JoH.tsl(); + if (chime_only) { + if (repeating) { + UserError.Log.d(TAG, "Rescheduling next"); + schedule_next(); + } else { + enabled = false; + } + } + save(); + } + + public synchronized void reminder_alert() { + Reminders.doAlert(this); + } + + public synchronized void schedule_next() { + this.next_due = this.next_due + this.period; + // check it is actually in the future + while (this.next_due < JoH.tsl()) { + this.next_due = this.next_due + this.period; + } + if (alternating) alternate = !alternate; + alerted_times = 0; // reset counter + save(); + } + + protected synchronized static void fixUpTable(String[] schema) { + if (patched) return; + for (String patch : schema) { + try { + SQLiteUtils.execSql(patch); + } catch (Exception e) { + // + } + } + patched = true; + } + + public static Reminder create(String title, long period) { + fixUpTable(schema); + Reminder reminder = new Reminder(); + reminder.title = title; + reminder.alternate_title = title + " alternate"; + reminder.period = period; + reminder.next_due = JoH.tsl() + period; + reminder.enabled = true; + reminder.snoozed_till = 0; + reminder.last_snoozed_for = 0; + reminder.last_fired = 0; + reminder.fired_times = 0; + reminder.repeating = true; + reminder.alternating = false; + reminder.alternate = false; + reminder.chime_only = false; + reminder.homeonly = false; + reminder.ideal_time = JoH.hourMinuteString(); + reminder.priority = 5; // default + reminder.save(); + return reminder; + } + + public static List getActiveReminders() { + fixUpTable(schema); + final long now = JoH.tsl(); + final List reminders = new Select() + .from(Reminder.class) + .where("enabled = ?", true) + .where("next_due < ?", now) + .where("snoozed_till < ?", now) + .orderBy("enabled desc, next_due asc") + .execute(); + return reminders; + } + + private static boolean isNight() { + final int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + return hour < 9; // midnight to 9am we say is night + } + + public static synchronized void processAnyDueReminders() { + if (JoH.quietratelimit("reminder_due_check", 10)) { + if (!Pref.getBooleanDefaultFalse(REMINDERS_ALL_DISABLED) + && (!Pref.getBooleanDefaultFalse(REMINDERS_NIGHT_DISABLED) || !isNight())) { + final Reminder due_reminder = getNextActiveReminder(); + if (due_reminder != null) { + UserError.Log.d(TAG, "Found due reminder! " + due_reminder.title); + due_reminder.reminder_alert(); + } + } else { + // reminders are disabled - should we re-enable them? + if (Pref.getBooleanDefaultFalse(REMINDERS_RESTART_TOMORROW)) { + // temporary testing logic + final int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + if (hour == 10) { + if (JoH.pratelimit("restart-reminders", 7200)) { + UserError.Log.d(TAG, "Re-enabling reminders as its morning time"); + Pref.setBoolean(REMINDERS_ALL_DISABLED, false); + } + } + } + } + } + } + + public static Reminder getNextActiveReminder() { + fixUpTable(schema); + final boolean onHomeWifi = !HomeWifi.isSet() || HomeWifi.isConnected(); + final long now = JoH.tsl(); + final Reminder reminder = new Select() + .from(Reminder.class) + .where("enabled = ?", true) + .where("next_due < ?", now) + .where("snoozed_till < ?", now) + .where("last_fired < (? - (600000 * alerted_times))", now) + // if on home wifi or not set then anything otherwise only home only = false + .where(onHomeWifi ? "homeonly > -1 " : "homeonly = 0") + .orderBy("enabled desc, priority desc, next_due asc") + .executeSingle(); + return reminder; + } + + public static List getAllReminders() { + fixUpTable(schema); + final List reminders = new Select() + .from(Reminder.class) + .orderBy("enabled desc, priority desc, next_due asc") + .execute(); + return reminders; + } + + public synchronized static void firstInit(Context context) { + fixUpTable(schema); + /* Inevitable.task("reminders-first-init", 2000, new Runnable() { + @Override + public void run() { + try { + final Reminder reminder = new Select() + .from(Reminder.class) + .where("enabled = ?", true) + .executeSingle(); + if (reminder != null) { + // PendingIntent serviceIntent = PendingIntent.getService(xdrip.getAppContext(), 0, new Intent(xdrip.getAppContext(), MissedReadingService.class), PendingIntent.FLAG_UPDATE_CURRENT); + // PendingIntent serviceIntent = WakeLockTrampoline.getPendingIntent(MissedReadingService.class); + // JoH.wakeUpIntent(xdrip.getAppContext(), Constants.MINUTE_IN_MS, serviceIntent); + // UserError.Log.ueh(TAG, "Starting missed readings service"); + } + } catch (NullPointerException e) { + UserError.Log.wtf(TAG, "Got nasty initial concurrency exception: " + e); + } + } + }); + */ + } + + public static Reminder byid(long id) { + return new Select() + .from(Reminder.class) + .where("_ID = ?", id) + .executeSingle(); + } + + + /*static Reminder getForPreciseTimestamp(double timestamp, double precision, String plugin) { + fixUpTable(schema); + final Reminder Reminder = new Select() + .from(Reminder.class) + .where("timestamp <= ?", (timestamp + precision)) + .where("timestamp >= ?", (timestamp - precision)) + .where("plugin = ?", plugin) + .orderBy("abs(timestamp - " + timestamp + ") asc") + .executeSingle(); + if (Reminder != null && Math.abs(Reminder.timestamp - timestamp) < precision) { + return Reminder; + } + return null; + } +*/ +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/RollCall.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/RollCall.java new file mode 100644 index 0000000..fcd7bd9 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/RollCall.java @@ -0,0 +1,309 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Build; + +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.UtilityModels.BridgeBattery; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; +import com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools; +import com.eveningoutpost.dexdrip.utils.CipherUtils; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; + +/** + * Created by jamorham on 20/01/2017. + */ + +public class RollCall { + + private static final String TAG = "RollCall"; + private static final int MAX_SSID_LENGTH = 20; + private static volatile HashMap indexed; + + @Expose + String device_manufactuer; + @Expose + String device_model; + @Expose + String device_serial; + @Expose + String device_name; + @Expose + String android_version; + @Expose + String xdrip_version; + @Expose + String role; + @Expose + String ssid; + @Expose + String mhint; + @Expose + int battery = -1; + @Expose + int bridge_battery = -1; + + // not set by instantiation + @Expose + String hash; + @Expose + Long last_seen; + + final long created = JoH.tsl(); + + public RollCall() { + this.device_manufactuer = Build.MANUFACTURER; + this.device_model = Build.MODEL; + this.device_name = JoH.getLocalBluetoothName(); // sanity check length + this.device_serial = Build.SERIAL; + this.android_version = Build.VERSION.RELEASE; + this.xdrip_version = JoH.getVersionDetails(); + + if (Home.get_follower()) { + this.role = "Follower"; + } else if (Home.get_master()) { + this.role = "Master"; + } else { + this.role = "None"; + } + + if (DesertSync.isEnabled()) { + try { + this.ssid = wifiString(); + } catch (Exception e) { + // + } + try { + if (this.role.equals("Master")) { + this.mhint = RouteTools.getBestInterfaceAddress(); + } + } catch (Exception e) { + // + } + } + } + + // populate with values from this device + public RollCall populate() { + this.battery = getBatteryLevel(); + this.bridge_battery = BridgeBattery.getBestBridgeBattery(); + return this; + } + + private boolean batteryValid() { + return battery != -1; + } + + private boolean bridgeBatteryValid() { + return bridge_battery > 0; + } + + private static String wifiString() { + String ssid = JoH.getWifiSSID(); + if (ssid != null && ssid.length() > MAX_SSID_LENGTH) { + ssid = ssid.substring(0, 20); + } + return ssid; + } + + @SuppressWarnings("ConstantConditions") + private static int getBatteryLevel() { + final Intent batteryIntent = xdrip.getAppContext().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + try { + final int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + final int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + if (level == -1 || scale == -1) { + return -1; + } + return (int) (((float) level / (float) scale) * 100.0f); + } catch (NullPointerException e) { + return -1; + } + } + + + private String getRemoteIpStatus() { + if (mhint != null) { + return "\n" + mhint; + } + return ""; + } + + private String getRemoteWifiIndicate(final String our_wifi_ssid) { + if (emptyString(our_wifi_ssid)) return ""; + if (emptyString(ssid)) return ""; + if (!our_wifi_ssid.equals(ssid)) return "\n" + ssid; + return ""; + } + + public String toS() { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + return gson.toJson(this); + } + + public String getHash() { + if (this.hash == null) { + this.hash = CipherUtils.getSHA256(this.device_manufactuer + this.android_version + this.device_model + this.device_serial); + } + return this.hash; + } + + public String bestName() { + if ((device_name != null) && (device_name.length() > 2)) { + return device_name; + } + return (!device_manufactuer.equals("unknown") ? device_manufactuer + " " : "") + device_model; + } + + public static RollCall fromJson(String json) { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + try { + return gson.fromJson(json, RollCall.class); + } catch (Exception e) { + UserError.Log.e(TAG, "Got exception processing fromJson() " + e); + UserError.Log.e(TAG, "json = " + json); + return null; + } + } + + public synchronized static void Seen(String item_json) { + try { + UserError.Log.d(TAG, "Processing Seen: " + item_json); + Seen(fromJson(item_json)); + } catch (Exception e) { + UserError.Log.e(TAG, "Got exception processing Seen() " + e); + } + } + + private synchronized static void Seen(RollCall item) { + // sanity check object contains some data + if (item == null) return; + if ((item.android_version == null) || (item.android_version.length() == 0)) return; + if (indexed == null) loadIndex(); + indexed.put(item.getHash(), item); + item.last_seen = JoH.tsl(); + saveIndex(); + } + + private static final String ROLLCALL_SAVED_INDEX = "RollCall-saved-index"; + + private static void saveIndex() { + final Gson gson = new GsonBuilder().create(); + final String[] array = new String[indexed.size()]; + int i = 0; + for (Map.Entry entry : indexed.entrySet()) { + array[i++] = (((RollCall) entry.getValue()).toS()); + } + PersistentStore.setString(ROLLCALL_SAVED_INDEX, gson.toJson(array)); + UserError.Log.d(TAG, "Saving"); + } + + private synchronized static void loadIndex() { + UserError.Log.d(TAG, "Loading index"); + final String loaded = PersistentStore.getString(ROLLCALL_SAVED_INDEX); + final HashMap hashmap = new HashMap<>(); + try { + if ((loaded != null) && (loaded.length() > 0)) { + final Gson gson = new GsonBuilder().create(); + final String[] array = gson.fromJson(loaded, String[].class); + if (array != null) { + for (String json : array) { + RollCall item = gson.fromJson(json, RollCall.class); + hashmap.put(item.getHash(), item); + } + } + } + } catch (Exception e) { + UserError.Log.e(TAG, "Error loading index: " + e); + } + indexed = hashmap; + UserError.Log.d(TAG, "Loaded: count: " + hashmap.size()); + } + + public static String getBestMasterHintIP() { + // TODO some intelligence regarding wifi ssid + //final String our_wifi_ssid = wifiString(); + if (indexed == null) loadIndex(); + RollCall bestMatch = null; + for (Map.Entry entry : indexed.entrySet()) { + final RollCall rc = (RollCall) entry.getValue(); + if (!rc.role.equals("Master")) continue; + if (emptyString(rc.mhint)) continue; + if (bestMatch == null || rc.last_seen > bestMatch.last_seen) { + bestMatch = rc; + } + } + UserError.Log.d(TAG, "Returning best master hint ip: " + (bestMatch != null ? bestMatch.toS() : "no match")); + return bestMatch != null ? bestMatch.mhint : null; + } + + + public static void pruneOld(int depth) { + if (indexed == null) loadIndex(); + if (depth > 10) return; + boolean changed = false; + for (Map.Entry entry : indexed.entrySet()) { + RollCall rc = (RollCall) entry.getValue(); + long since = JoH.msSince(rc.last_seen); + + if ((since < 0) || (since > (1000 * 60 * 60 * 24))) { + UserError.Log.d(TAG, "Pruning entry: " + rc.bestName()); + indexed.remove(entry.getKey().toString()); + changed = true; + break; + } + } + if (changed) { + saveIndex(); + pruneOld(depth + 1); + } + } + + // data for MegaStatus + public static List megaStatus() { + if (indexed == null) loadIndex(); + GcmActivity.requestRollCall(); + // TODO sort data + final boolean engineering = Home.get_engineering_mode(); + final boolean desert_sync = DesertSync.isEnabled(); + final String our_wifi_ssid = desert_sync ? wifiString() : ""; + final List lf = new ArrayList<>(); + for (Map.Entry entry : indexed.entrySet()) { + final RollCall rc = (RollCall) entry.getValue(); + // TODO refactor with stringbuilder + lf.add(new StatusItem(rc.role + (desert_sync ? rc.getRemoteWifiIndicate(our_wifi_ssid) : "") + (engineering ? ("\n" + JoH.niceTimeSince(rc.last_seen) + " ago") : ""), rc.bestName() + (desert_sync ? rc.getRemoteIpStatus() : "") + (engineering && rc.batteryValid() ? ("\n" + rc.battery + "%") : "") + (engineering && rc.bridgeBatteryValid() ? (" " + rc.bridge_battery+"%") : ""))); + } + + Collections.sort(lf, new Comparator() { + public int compare(StatusItem left, StatusItem right) { + int val = right.name.replaceFirst("\n.*$", "").compareTo(left.name.replaceFirst("\n.*$", "")); // descending sort ignore second line + if (val == 0) val = left.value.compareTo(right.value); // ascending sort + return val; + } + }); + // TODO could scan for duplicates and append serial to bestName + + return new ArrayList<>(lf); + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Sensor.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Sensor.java new file mode 100644 index 0000000..20d3696 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Sensor.java @@ -0,0 +1,301 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.SensorSendQueue; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.google.gson.internal.bind.DateTypeAdapter; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * Created by Emma Black on 10/29/14. + */ + +@Table(name = "Sensors", id = BaseColumns._ID) +public class Sensor extends Model { + private final static String TAG = Sensor.class.getSimpleName(); + + @Expose + @Column(name = "started_at", index = true) + public long started_at; + + @Expose + @Column(name = "stopped_at") + public long stopped_at; + + @Expose + //latest minimal battery level + @Column(name = "latest_battery_level") + public int latest_battery_level; + + @Expose + @Column(name = "uuid", index = true) + public String uuid; + + @Expose + @Column(name = "sensor_location") + public String sensor_location; + + public static Sensor create(long started_at) { + Sensor sensor = new Sensor(); + sensor.started_at = started_at; + sensor.uuid = UUID.randomUUID().toString(); + + sensor.save(); + SensorSendQueue.addToQueue(sensor); + Log.d("SENSOR MODEL:", sensor.toString()); + return sensor; + } + + public static Sensor create(long started_at, String uuid) {//KS + Sensor sensor = new Sensor(); + sensor.started_at = started_at; + sensor.uuid = uuid; + + sensor.save(); + SensorSendQueue.addToQueue(sensor); + Log.d("SENSOR MODEL:", sensor.toString()); + return sensor; + } + + public static Sensor createDefaultIfMissing() { + final Sensor sensor = currentSensor(); + if (sensor == null) { + Sensor.create(JoH.tsl()); + } + return currentSensor(); + } + + // Used by xDripViewer + public static void createUpdate(long started_at, long stopped_at, int latest_battery_level, String uuid) { + + Sensor sensor = getByTimestamp(started_at); + if (sensor != null) { + Log.d("SENSOR", "updatinga an existing sensor"); + } else { + Log.d("SENSOR", "creating a new sensor"); + sensor = new Sensor(); + } + sensor.started_at = started_at; + sensor.stopped_at = stopped_at; + sensor.latest_battery_level = latest_battery_level; + sensor.uuid = uuid; + sensor.save(); + } + + public synchronized static void stopSensor() { + final Sensor sensor = currentSensor(); + if (sensor == null) { + return; + } + sensor.stopped_at = JoH.tsl(); + UserError.Log.ueh("SENSOR", "Sensor stopped at " + JoH.dateTimeText(sensor.stopped_at)); + sensor.save(); + if (currentSensor() != null) { + UserError.Log.wtf(TAG, "Failed to update sensor stop in database"); + } + SensorSendQueue.addToQueue(sensor); + JoH.clearCache(); + + } + + public String toS() {//KS + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + Log.d("SENSOR", "Sensor toS uuid=" + this.uuid + " started_at=" + this.started_at + " active=" + this.isActive() + " battery=" + this.latest_battery_level + " location=" + this.sensor_location + " stopped_at=" + this.stopped_at); + return gson.toJson(this); + } + + public static Sensor lastStopped() { + Sensor sensor = new Select() + .from(Sensor.class) + .where("started_at != 0") + .where("stopped_at != 0") + .orderBy("_ID desc") + .limit(1) + .executeSingle(); + return sensor; + } + + public static boolean stoppedRecently() { + final Sensor last = lastStopped(); + return last != null && last.stopped_at < JoH.tsl() && (JoH.msSince(last.stopped_at) < (Constants.HOUR_IN_MS * 2)); + } + + public static Sensor currentSensor() { + Sensor sensor = new Select() + .from(Sensor.class) + .where("started_at != 0") + .where("stopped_at = 0") + .orderBy("_ID desc") + .limit(1) + .executeSingle(); + return sensor; + } + + public static boolean isActive() { + Sensor sensor = new Select() + .from(Sensor.class) + .where("started_at != 0") + .where("stopped_at = 0") + .orderBy("_ID desc") + .limit(1) + .executeSingle(); + if(sensor == null) { + return false; + } else { + return true; + } + } + + public static Sensor getByTimestamp(double started_at) { + return new Select() + .from(Sensor.class) + .where("started_at = ?", started_at) + .executeSingle(); + } + + public static Sensor getByUuid(String xDrip_sensor_uuid) { + if(xDrip_sensor_uuid == null) { + Log.e("SENSOR", "xDrip_sensor_uuid is null"); + return null; + } + Log.d("SENSOR", "xDrip_sensor_uuid is " + xDrip_sensor_uuid); + + return new Select() + .from(Sensor.class) + .where("uuid = ?", xDrip_sensor_uuid) + .executeSingle(); + } + + public static void updateBatteryLevel(int sensorBatteryLevel, boolean from_sync) { + Sensor sensor = Sensor.currentSensor(); + if (sensor == null) + { + Log.d("Sensor","Cant sync battery level from master as sensor data is null"); + return; + } + updateBatteryLevel(sensor, sensorBatteryLevel, from_sync); + } + + public static void updateBatteryLevel(Sensor sensor, int sensorBatteryLevel) { + updateBatteryLevel(sensor, sensorBatteryLevel, false); + } + + public static void updateBatteryLevel(Sensor sensor, int sensorBatteryLevel, boolean from_sync) { + if (sensorBatteryLevel < 120) { + // This must be a wrong battery level. Some transmitter send those every couple of readings + // even if the battery is ok. + return; + } + int startBatteryLevel = sensor.latest_battery_level; + // if(sensor.latest_battery_level == 0) { + // allow sensor battery level to go up and down + sensor.latest_battery_level = sensorBatteryLevel; + // } else { + // sensor.latest_battery_level = Math.min(sensor.latest_battery_level, sensorBatteryLevel); + // } + if (startBatteryLevel == sensor.latest_battery_level) { + // no need to update anything if nothing has changed. + return; + } + sensor.save(); + SensorSendQueue.addToQueue(sensor); + if ((!from_sync) && (Home.get_master())) { + GcmActivity.sendSensorBattery(sensor.latest_battery_level); + } + } + + public static void updateSensorLocation(String sensor_location) { + Sensor sensor = currentSensor(); + if (sensor == null) { + Log.e("SENSOR MODEL:", "updateSensorLocation called but sensor is null"); + return; + } + sensor.sensor_location = sensor_location; + sensor.save(); + } + + public static void upsertFromMaster(Sensor jsonSensor) { + if (jsonSensor == null) { + Log.wtf(TAG,"Got null sensor from json"); + return; + } + try { + Sensor existingSensor = getByUuid(jsonSensor.uuid); + if (existingSensor == null) { + Log.d(TAG, "saving new sensor record."); + jsonSensor.save(); + } else { + Log.d(TAG, "updating existing sensor record."); + existingSensor.started_at = jsonSensor.started_at; + existingSensor.stopped_at = jsonSensor.stopped_at; + existingSensor.latest_battery_level = jsonSensor.latest_battery_level; + existingSensor.sensor_location = jsonSensor.sensor_location; + existingSensor.save(); + } + } catch (Exception e) { + Log.e(TAG, "Could not save Sensor: " + e.toString()); + } + } + + public String toJSON() { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("started_at", started_at); + jsonObject.put("stopped_at", stopped_at); + jsonObject.put("latest_battery_level", latest_battery_level); + jsonObject.put("uuid", uuid); + jsonObject.put("sensor_location", sensor_location); + return jsonObject.toString(); + } catch (JSONException e) { + Log.e(TAG,"Got JSONException handeling sensor", e); + return ""; + } + } + + public static Sensor fromJSON(String json) { + if (json.length()==0) { + Log.d(TAG,"Empty json received in Sensor fromJson"); + return null; + } + try { + Log.d(TAG, "Processing incoming json: " + json); + return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json,Sensor.class); + } catch (Exception e) { + Log.d(TAG, "Got exception parsing Sensor json: " + e.toString()); + Home.toaststaticnext("Error on Sensor sync."); + return null; + } + } + + + public static void shutdownAllSensors() { + final List l = new Select().from(Sensor.class).execute(); + for (final Sensor s : l) { + s.stopped_at = s.started_at; + s.save(); + System.out.println(s.toJSON()); + } + } +} + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/SensorSanity.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/SensorSanity.java new file mode 100644 index 0000000..4ac8aaa --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/SensorSanity.java @@ -0,0 +1,130 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; + +/** + * Created by jamorham on 02/03/2018. + * + * Checks for whether sensor data is within a sane range + * + */ + +public class SensorSanity { + + public static final double DEXCOM_MIN_RAW = 5; // raw values below this will be treated as error + public static final double DEXCOM_MAX_RAW = 1000; // raw values above this will be treated as error + + public static final double DEXCOM_G6_MIN_RAW = 5; // raw values below this will be treated as error + public static final double DEXCOM_G6_MAX_RAW = 1000; // raw values above this will be treated as error + + public static final double LIBRE_MIN_RAW = 5; // raw values below this will be treated as error + + private static final String TAG = "SensorSanity"; + + public static boolean isRawValueSane(double raw_value) { + return isRawValueSane(raw_value, DexCollectionType.getDexCollectionType(), false); + } + + public static boolean isRawValueSane(double raw_value, boolean hard) { + return isRawValueSane(raw_value, DexCollectionType.getDexCollectionType(), hard); + } + + public static boolean isRawValueSane(double raw_value, DexCollectionType type) { + return isRawValueSane(raw_value, type, false); + + } + + public static boolean isRawValueSane(double raw_value, DexCollectionType type, boolean hard_check) { + + // bypass checks if the allowing dead sensor engineering mode is enabled + if (allowTestingWithDeadSensor()) { + if (JoH.pratelimit("dead-sensor-sanity-passing", 3600)) { + UserError.Log.e(TAG, "Allowing any value due to Allow Dead Sensor being enabled"); + } + return true; + } + + // passes by default! + boolean state = true; + + // checks for each type of data source + + if (DexCollectionType.hasDexcomRaw(type)) { + if (!BgReading.isRawMarkerValue(raw_value) || hard_check) { + if (Pref.getBooleanDefaultFalse("using_g6")) { + if (raw_value < DEXCOM_G6_MIN_RAW) state = false; + else if (raw_value > DEXCOM_G6_MAX_RAW) state = false; + } else { + if (raw_value < DEXCOM_MIN_RAW) state = false; + else if (raw_value > DEXCOM_MAX_RAW) state = false; + } + } + + } else if (DexCollectionType.hasLibre(type)) { + if (raw_value < LIBRE_MIN_RAW) state = false; + } else if (type == DexCollectionType.Medtrum) { + if (raw_value < DEXCOM_MIN_RAW) state = false; + else if (raw_value > DEXCOM_MAX_RAW) state = false; + } + + if (!state) { + if (JoH.ratelimit("sanity-failure", 20)) { + final String msg = "Sensor Raw Data Sanity Failure: " + raw_value; + UserError.Log.e(TAG, msg); + JoH.static_toast_long(msg); + } + } + + return state; + } + + public static boolean allowTestingWithDeadSensor() { + return Pref.getBooleanDefaultFalse("allow_testing_with_dead_sensor") + && Pref.getBooleanDefaultFalse("engineering_mode"); + } + + + /** + * Check Libre serial for unexpected changes. Stop xDrip sensor session if there is a mismatch. + * + * Same sensor session with different sensor serial number results in error response + * + * boolean return of true indicates problem + */ + + private static final String PREF_LIBRE_SN = "SensorSanity-LibreSN"; + private static final String PREF_LIBRE_SENSOR_UUID = "SensorSanity-LibreSensor"; + + public static boolean checkLibreSensorChangeIfEnabled(final String sn) { + return Pref.getBoolean("detect_libre_sn_changes", true) && checkLibreSensorChange(sn); + } + + public synchronized static boolean checkLibreSensorChange(final String currentSerial) { + if ((currentSerial == null) || currentSerial.length() < 4) return false; + final String lastSn = PersistentStore.getString(PREF_LIBRE_SN); + if (!currentSerial.equals(lastSn)) { + final Sensor this_sensor = Sensor.currentSensor(); + if ((lastSn.length() > 3) && (this_sensor != null)) { + + final String last_uuid = PersistentStore.getString(PREF_LIBRE_SENSOR_UUID); + + if (last_uuid.equals(this_sensor.uuid)) { + if (last_uuid.length() > 3) { + UserError.Log.wtf(TAG, String.format("Different sensor serial number for same sensor uuid: %s :: %s vs %s", last_uuid, lastSn, currentSerial)); + Sensor.stopSensor(); + JoH.static_toast_long("Stopping sensor due to serial number change"); + Sensor.stopSensor(); + return true; + } + } else { + PersistentStore.setString(PREF_LIBRE_SENSOR_UUID, this_sensor.uuid); + } + } + PersistentStore.setString(PREF_LIBRE_SN, currentSerial); + } + return false; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/StepCounter.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/StepCounter.java new file mode 100644 index 0000000..c9f6d79 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/StepCounter.java @@ -0,0 +1,164 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by jamorham on 01/11/2016. + */ + + +@Table(name = "PebbleMovement", id = BaseColumns._ID) +public class StepCounter extends Model { + + private static boolean patched = false; + private final static String TAG = "StepCounter"; + private final static boolean d = false; + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "metric") + public int metric; + + + // patches and saves + public Long saveit() { + try { + fixUpTable(); + return save(); + } catch (Exception e) { + return null; + } + } + + public String toS() { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + return gson.toJson(this); + } + + + // static methods + + public static StepCounter createEfficientRecord(long timestamp_ms, int data) + { + StepCounter pm = last(); + if ((pm == null) || (data < pm.metric) || ((timestamp_ms - pm.timestamp) > (1000 * 30 * 5))) { + pm = new StepCounter(); + pm.timestamp = timestamp_ms; + if (d) UserError.Log.d(TAG,"Creating new record for timestamp: "+JoH.dateTimeText(timestamp_ms)); + } else { + if (d) UserError.Log.d(TAG,"Merging pebble movement record: "+JoH.dateTimeText(timestamp_ms)+" vs old "+JoH.dateTimeText(pm.timestamp)); + } + + pm.metric = (int) (long) data; + if(d) UserError.Log.d(TAG, "Saving Movement: " + pm.toS()); + pm.saveit(); + return pm; + } + + public static StepCounter last() { + try { + return new Select() + .from(StepCounter.class) + .orderBy("timestamp desc") + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + return new Select() + .from(StepCounter.class) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return new ArrayList<>(); + } + } + + // expects pre-sorted in asc order? + public static List deltaListFromMovementList(List mList) { + int last_metric = -1; + int temp_metric = -1; + for (StepCounter pm : mList) { + // first item in list + if (last_metric == -1) { + last_metric = pm.metric; + pm.metric = 0; + } else { + // normal incrementing calculate delta + if (pm.metric >= last_metric) { + temp_metric = pm.metric - last_metric; + last_metric = pm.metric; + pm.metric = temp_metric; + } else { + last_metric = pm.metric; + } + } + } + return mList; + } + + public static List cleanup(int retention_days) { + return new Delete() + .from(StepCounter.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L)) + .execute(); + } + + + // create the table ourselves without worrying about model versioning and downgrading + private static void fixUpTable() { + if (patched) return; + String[] patchup = { + "CREATE TABLE PebbleMovement (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE PebbleMovement ADD COLUMN timestamp INTEGER;", + "ALTER TABLE PebbleMovement ADD COLUMN metric INTEGER;", + "CREATE UNIQUE INDEX index_PebbleMovement_timestamp on PebbleMovement(timestamp);"}; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + // UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } +} + + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Tomato.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Tomato.java new file mode 100644 index 0000000..bee528e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Tomato.java @@ -0,0 +1,258 @@ +package com.eveningoutpost.dexdrip.Models; + +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.NFCReaderX; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.UtilityModels.Blukon; +import com.eveningoutpost.dexdrip.UtilityModels.BridgeResponse; +import com.eveningoutpost.dexdrip.UtilityModels.LibreUtils; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import static com.eveningoutpost.dexdrip.xdrip.gs; +/** + * Created by Tzachi Dar on 7.3.2018. + */ + +public class Tomato { + private static final String TAG = "DexCollectionService";//?????"Tomato"; + + private static final String CHECKSUM_FAILED = "checksum failed"; + private static final String SERIAL_FAILED = "serial failed"; + + private enum TOMATO_STATES { + REQUEST_DATA_SENT, + RECIEVING_DATA + } + private final static int TOMATO_HEADER_LENGTH = 18; + private final static int TOMATO_PATCH_INFO = 6; + + + static volatile TOMATO_STATES s_state; + + private static volatile long s_lastReceiveTimestamp; + private static volatile byte[] s_full_data = null; + private static volatile int s_acumulatedSize = 0; + private static volatile boolean s_recviedEnoughData; + + + public static boolean isTomato() { + final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + if (activeBluetoothDevice == null || activeBluetoothDevice.name == null) { + return false; + } + + return activeBluetoothDevice.name.startsWith("miaomiao") + || activeBluetoothDevice.name.toLowerCase().startsWith("watlaa"); + } + + public static BridgeResponse decodeTomatoPacket(byte[] buffer, int len) { + final BridgeResponse reply = new BridgeResponse(); + // Check time, probably need to start on sending + long now = JoH.tsl(); + if(now - s_lastReceiveTimestamp > 3*1000) { + // We did not receive data in 3 seconds, moving to init state again + Log.e(TAG, "Recieved a buffer after " + (now - s_lastReceiveTimestamp) / 1000 + " seconds, starting again. "+ + "already acumulated " + s_acumulatedSize + " bytes."); + s_state = TOMATO_STATES.REQUEST_DATA_SENT; + } + + s_lastReceiveTimestamp = now; + if (buffer == null) { + Log.e(TAG, "null buffer passed to decodeTomatoPacket"); + return reply; + } + if (s_state == TOMATO_STATES.REQUEST_DATA_SENT) { + if(buffer.length == 1 && buffer[0] == 0x32) { + Log.e(TAG, "returning allow sensor confirm"); + + ByteBuffer allowNewSensor = ByteBuffer.allocate(2); + allowNewSensor.put(0, (byte) 0xD3); + allowNewSensor.put(1, (byte) 0x01); + reply.add(allowNewSensor); + + // For debug, make it send data every minute (did not work...) + ByteBuffer newFreqMessage = ByteBuffer.allocate(2); + newFreqMessage.put(0, (byte) 0xD1); + newFreqMessage.put(1, (byte) 0x05); + reply.add(newFreqMessage); + + //command to start reading + ByteBuffer ackMessage = ByteBuffer.allocate(1); + ackMessage.put(0, (byte) 0xF0); + reply.add(ackMessage); + return reply; + } + + if(buffer.length == 1 && buffer[0] == 0x34) { + Log.e(TAG, "No sensor has been found"); + reply.setError_message(gs(R.string.no_sensor_found)); + return reply; + } + + // 18 is the expected header size + if(buffer.length >= TOMATO_HEADER_LENGTH && buffer[0] == 0x28) { + // We are starting to receive data, need to start accumulating + + // &0xff is needed to convert to hex. + int expectedSize = 256 * (int)(buffer[1] & 0xFF) + (int)(buffer[2] & 0xFF); + Log.e(TAG, "Starting to acumulate data expectedSize = " + expectedSize); + InitBuffer(expectedSize + TOMATO_PATCH_INFO); + addData(buffer); + s_state = TOMATO_STATES.RECIEVING_DATA; + return reply; + + } else { + if (JoH.quietratelimit("unknown-initial-packet", 1)) { + Log.d(TAG,"Unknown initial packet makeup received" + HexDump.dumpHexString(buffer)); + } + return reply; + } + } + + if (s_state == TOMATO_STATES.RECIEVING_DATA) { + //Log.e(TAG, "received more data s_acumulatedSize = " + s_acumulatedSize + " current buffer size " + buffer.length); + try { + addData(buffer); + } catch (RuntimeException e) { + // if the checksum failed lets ask for the data set again but not more than once per minute + if (e.getMessage().equals(CHECKSUM_FAILED)) { + if (JoH.ratelimit("tomato-full-retry",60) + || JoH.ratelimit("tomato-full-retry2",60)) { + reply.getSend().clear(); + reply.getSend().addAll(Tomato.resetTomatoState()); + reply.setDelay(8000); + reply.setError_message(gs(R.string.checksum_failed__retrying)); + Log.d(TAG,"Asking for retry of data"); + } + } else if (e.getMessage().equals(SERIAL_FAILED)) { + reply.setError_message("Sensor Serial Problem"); + } else throw e; + } + + return reply; + } + + Log.wtf(TAG, "Very strange, In an unexpected state " + s_state); + + return reply; + } + + static void addData(byte[] buffer) { + if(s_acumulatedSize + buffer.length > s_full_data.length) { + Log.e(TAG, "Error recieving too much data. exiting. s_acumulatedSize = " + s_acumulatedSize + + " buffer.length = " + buffer.length + " s_full_data.length " + s_full_data.length); + //??? send something to start back?? + return; + + } + System.arraycopy(buffer, 0, s_full_data, s_acumulatedSize, buffer.length); + s_acumulatedSize += buffer.length; + AreWeDone(); + } + + static void AreWeDone() { + // Give both versions a chance to work. + final int extended_length = 344 + TOMATO_HEADER_LENGTH + 1 + TOMATO_PATCH_INFO; + if(s_recviedEnoughData && (s_acumulatedSize != extended_length)) { + // This reading already ended + Log.e(TAG,"Getting out, as s_recviedEnoughData and we have too much data already s_acumulatedSize = " + s_acumulatedSize); + return; + } + + if(s_acumulatedSize < 344 + TOMATO_HEADER_LENGTH + 1 ) { + //Log.e(TAG,"Getting out, since not enough data s_acumulatedSize = " + s_acumulatedSize); + return; + } + byte[] data = Arrays.copyOfRange(s_full_data, TOMATO_HEADER_LENGTH, TOMATO_HEADER_LENGTH+344); + s_recviedEnoughData = true; + + long now = JoH.tsl(); + // Important note, the actual serial number is 8 bytes long and starts at addresses 5. + final String SensorSn = LibreUtils.decodeSerialNumberKey(Arrays.copyOfRange(s_full_data, 5, 13)); + byte []patchUid = null; + byte []patchInfo = null; + if(s_acumulatedSize >= extended_length) { + patchUid = Arrays.copyOfRange(s_full_data, 5, 13); + patchInfo = Arrays.copyOfRange(s_full_data, TOMATO_HEADER_LENGTH+ 344 + 1 , TOMATO_HEADER_LENGTH + 344 + 1+ TOMATO_PATCH_INFO); + } + Log.d(TAG, "patchUid = " + HexDump.dumpHexString(patchUid)); + Log.d(TAG, "patchInfo = " + HexDump.dumpHexString(patchInfo)); + boolean checksum_ok = NFCReaderX.HandleGoodReading(SensorSn, data, now, true, patchUid, patchInfo); + Log.e(TAG, "We have all the data that we need " + s_acumulatedSize + " checksum_ok = " + checksum_ok + HexDump.dumpHexString(data)); + + if(!checksum_ok) { + throw new RuntimeException(CHECKSUM_FAILED); + } + + if (SensorSanity.checkLibreSensorChangeIfEnabled(SensorSn)) { + Log.e(TAG,"Problem with Libre Serial Number - not processing"); + throw new RuntimeException(SERIAL_FAILED); + } + + PersistentStore.setString("Tomatobattery", Integer.toString(s_full_data[13])); + Pref.setInt("bridge_battery", s_full_data[13]); + PersistentStore.setString("TomatoHArdware",HexDump.toHexString(s_full_data,16,2)); + PersistentStore.setString("TomatoFirmware",HexDump.toHexString(s_full_data,14,2)); + PersistentStore.setString("LibreSN", SensorSn); + + + } + + // This is the function that we should have once we are able to read all data realiably. + static void AreWeDoneMax() { + + if(s_acumulatedSize == s_full_data.length) { + Log.e(TAG, "We have a full packet"); + } else { + return; + } + if(s_full_data[s_full_data.length -1] != 0x29) { + Log.e(TAG, "recieved full data, but last byte is not 0x29. It is " + s_full_data[s_full_data.length -1]); + return; + } + // We have all the data + if(s_full_data.length < 344 + TOMATO_HEADER_LENGTH + 1) { + Log.e(TAG, "We have all the data, but it is not enough... s_full_data.length = " + s_full_data.length ); + return; + } + Log.e(TAG, "We have a full packet"); + + } + + + static void InitBuffer(int expectedSize) { + s_full_data = new byte[expectedSize]; + s_acumulatedSize = 0; + s_recviedEnoughData = false; + + } + + public static ArrayList initialize() { + Log.i(TAG, "initialize!"); + Pref.setInt("bridge_battery", 0); //force battery to no-value before first reading + return resetTomatoState(); + } + + private static ArrayList resetTomatoState() { + ArrayList ret = new ArrayList<>(); + + s_state = TOMATO_STATES.REQUEST_DATA_SENT; + + // Make tomato send data every 5 minutes + ByteBuffer newFreqMessage = ByteBuffer.allocate(2); + newFreqMessage.put(0, (byte) 0xD1); + newFreqMessage.put(1, (byte) 0x05); + ret.add(newFreqMessage); + + //command to start reading + ByteBuffer ackMessage = ByteBuffer.allocate(1); + ackMessage.put(0, (byte) 0xF0); + ret.add(ackMessage); + return ret; + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/TransmitterData.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/TransmitterData.java new file mode 100644 index 0000000..9edd0ae --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/TransmitterData.java @@ -0,0 +1,277 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.utils.CheckBridgeBattery; +import com.eveningoutpost.dexdrip.utils.DexCollectionType; +import com.google.gson.annotations.Expose; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * Created by Emma Black on 11/6/14. + */ + +@Table(name = "TransmitterData", id = BaseColumns._ID) +public class TransmitterData extends Model { + private final static String TAG = TransmitterData.class.getSimpleName(); + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + + // TODO these should be int or long surely + @Expose + @Column(name = "raw_data") + public double raw_data; + + @Expose + @Column(name = "filtered_data") + public double filtered_data; + + @Expose + @Column(name = "sensor_battery_level") + public int sensor_battery_level; + + @Expose + @Column(name = "uuid", index = true) + public String uuid; + + public static synchronized TransmitterData create(byte[] buffer, int len, Long timestamp) { + if (len < 6) { + return null; + } + final TransmitterData transmitterData = new TransmitterData(); + try { + if ((buffer[0] == 0x11 || buffer[0] == 0x15) && buffer[1] == 0x00) { + //this is a dexbridge packet. Process accordingly. + Log.i(TAG, "create Processing a Dexbridge packet"); + final ByteBuffer txData = ByteBuffer.allocate(len); + txData.order(ByteOrder.LITTLE_ENDIAN); + txData.put(buffer, 0, len); + transmitterData.raw_data = txData.getInt(2); + transmitterData.filtered_data = txData.getInt(6); + // bitwise and with 0xff (1111....1) to avoid that the byte is treated as signed. + transmitterData.sensor_battery_level = txData.get(10) & 0xff; + if (buffer[0] == 0x15) { + Log.i(TAG, "create Processing a Dexbridge packet includes delay information"); + transmitterData.timestamp = timestamp - txData.getInt(16); + } else { + transmitterData.timestamp = timestamp; + } + Log.i(TAG, "Created transmitterData record with Raw value of " + transmitterData.raw_data + " and Filtered value of " + transmitterData.filtered_data + " at " + timestamp + " with timestamp " + transmitterData.timestamp); + } else { //this is NOT a dexbridge packet. Process accordingly. + Log.i(TAG, "create Processing a BTWixel or IPWixel packet"); + StringBuilder data_string = new StringBuilder(); + for (int i = 0; i < len; ++i) { + data_string.append((char) buffer[i]); + } + final String[] data = data_string.toString().split("\\s+"); + + if (data.length > 1) { + transmitterData.sensor_battery_level = Integer.parseInt(data[1]); + if (data.length > 2) { + try { + Pref.setInt("bridge_battery", Integer.parseInt(data[2])); + if (Home.get_master()) { + GcmActivity.sendBridgeBattery(Pref.getInt("bridge_battery", -1)); + } + CheckBridgeBattery.checkBridgeBattery(); + } catch (Exception e) { + Log.e(TAG, "Got exception processing classic wixel or limitter battery value: " + e.toString()); + } + if (data.length > 3) { + if ((DexCollectionType.getDexCollectionType() == DexCollectionType.LimiTTer) + && (!Pref.getBooleanDefaultFalse("use_transmiter_pl_bluetooth"))) { + try { + // reported sensor age in minutes + final Integer sensorAge = Integer.parseInt(data[3]); + if ((sensorAge > 0) && (sensorAge < 200000)) + Pref.setInt("nfc_sensor_age", sensorAge); + } catch (Exception e) { + Log.e(TAG, "Got exception processing field 4 in classic limitter protocol: " + e); + } + } + } + } + } + transmitterData.raw_data = Integer.parseInt(data[0]); + transmitterData.filtered_data = Integer.parseInt(data[0]); + // TODO process does_have_filtered_here with extended protocol + transmitterData.timestamp = timestamp; + } + + //Stop allowing readings that are older than the last one - or duplicate data, its bad! (from savek-cc) + final TransmitterData lastTransmitterData = TransmitterData.last(); + if (lastTransmitterData != null && lastTransmitterData.timestamp >= transmitterData.timestamp) { + Log.e(TAG, "Rejecting TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " >= this: " + JoH.dateTimeText(transmitterData.timestamp)); + return null; + } + if (lastTransmitterData != null && lastTransmitterData.raw_data == transmitterData.raw_data && Math.abs(lastTransmitterData.timestamp - transmitterData.timestamp) < (Constants.MINUTE_IN_MS * 2)) { + Log.e(TAG, "Rejecting identical TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " due to 2 minute rule this: " + JoH.dateTimeText(transmitterData.timestamp)); + return null; + } + final Calibration lastCalibration = Calibration.lastValid(); + if (lastCalibration != null && lastCalibration.timestamp > transmitterData.timestamp) { + Log.e(TAG, "Rejecting historical TransmitterData constraint: calib: " + JoH.dateTimeText(lastCalibration.timestamp) + " > this: " + JoH.dateTimeText(transmitterData.timestamp)); + return null; + } + + transmitterData.uuid = UUID.randomUUID().toString(); + transmitterData.save(); + return transmitterData; + } catch (Exception e) { + Log.e(TAG, "Got exception processing fields in protocol: " + e + " " + HexDump.dumpHexString(buffer)); + } + return null; + } + + public static synchronized TransmitterData create(int raw_data, int filtered_data, int sensor_battery_level, long timestamp) { + TransmitterData lastTransmitterData = TransmitterData.last(); + if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad! + return null; + } + + TransmitterData transmitterData = new TransmitterData(); + transmitterData.sensor_battery_level = sensor_battery_level; + transmitterData.raw_data = raw_data; + transmitterData.filtered_data = filtered_data; + transmitterData.timestamp = timestamp; + transmitterData.uuid = UUID.randomUUID().toString(); + transmitterData.save(); + return transmitterData; + } + + public static synchronized TransmitterData create(int raw_data ,int sensor_battery_level, long timestamp) { + TransmitterData lastTransmitterData = TransmitterData.last(); + if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad! + return null; + } + + TransmitterData transmitterData = new TransmitterData(); + transmitterData.sensor_battery_level = sensor_battery_level; + transmitterData.raw_data = raw_data ; + transmitterData.timestamp = timestamp; + transmitterData.uuid = UUID.randomUUID().toString(); + transmitterData.save(); + return transmitterData; + } + + public static TransmitterData last() { + return new Select() + .from(TransmitterData.class) + .orderBy("_ID desc") + .executeSingle(); + } + + public static List last(int count) { + return new Select() + .from(TransmitterData.class) + .orderBy("_ID desc") + .limit(count) + .execute(); + } + + public static TransmitterData lastByTimestamp() { + return new Select() + .from(TransmitterData.class) + .orderBy("timestamp desc") + .executeSingle(); + } + + public static TransmitterData getForTimestamp(double timestamp) {//KS + try { + Sensor sensor = Sensor.currentSensor(); + if (sensor != null) { + TransmitterData bgReading = new Select() + .from(TransmitterData.class) + .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) + .orderBy("timestamp desc") + .executeSingle(); + if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? + Log.i(TAG, "getForTimestamp: Found a BG timestamp match"); + return bgReading; + } + } + } catch (Exception e) { + Log.e(TAG,"getForTimestamp() Got exception on Select : "+e.toString()); + return null; + } + Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match"); + return null; + } + + public static TransmitterData findByUuid(String uuid) {//KS + try { + return new Select() + .from(TransmitterData.class) + .where("uuid = ?", uuid) + .executeSingle(); + } catch (Exception e) { + Log.e(TAG,"findByUuid() Got exception on Select : "+e.toString()); + return null; + } + } + + public static TransmitterData byid(long id) { + return new Select() + .from(TransmitterData.class) + .where("_ID = ?", id) + .executeSingle(); + } + + public static void updateTransmitterBatteryFromSync(final int battery_level) { + try { + TransmitterData td = TransmitterData.last(); + if ((td == null) || (td.raw_data!=0)) + { + td=TransmitterData.create(0,battery_level,(long)JoH.ts()); + Log.d(TAG,"Created new fake transmitter data record for battery sync"); + if (td==null) return; + } + if ((battery_level != td.sensor_battery_level) || ((JoH.ts()-td.timestamp)>(1000*60*60))) { + td.sensor_battery_level = battery_level; + td.timestamp = (long)JoH.ts(); // freshen timestamp on this bogus record for system status + Log.d(TAG,"Saving synced sensor battery, new level: "+battery_level); + td.save(); + } else { + Log.d(TAG,"Synced sensor battery level same as existing: "+battery_level); + } + } catch (Exception e) { + Log.e(TAG,"Got exception updating sensor battery from sync: "+e.toString()); + } + } + + private static double roundRaw(TransmitterData td) { + return JoH.roundDouble(td.raw_data,3); + } + private static double roundFiltered(TransmitterData td) { + return JoH.roundDouble(td.filtered_data,3); + } + + public static boolean unchangedRaw() { + final List items = last(3); + if (items != null && items.size() == 3) { + return (roundRaw(items.get(0)) == roundRaw(items.get(1)) + && roundRaw(items.get(0)) == roundRaw(items.get(2)) + && roundFiltered(items.get(0)) == roundFiltered(items.get(1)) + && roundFiltered(items.get(0)) == roundFiltered(items.get(2))); + } + return false; + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java new file mode 100644 index 0000000..30364b8 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java @@ -0,0 +1,1252 @@ +package com.eveningoutpost.dexdrip.Models; + +/** + * Created by jamorham on 31/12/15. + */ + +import android.content.Context; +import android.provider.BaseColumns; +import android.util.Pair; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import com.eveningoutpost.dexdrip.GcmActivity; +import com.eveningoutpost.dexdrip.Home; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.Services.SyncService; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.UtilityModels.UndoRedo; +import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue; +import com.eveningoutpost.dexdrip.insulin.Insulin; +import com.eveningoutpost.dexdrip.insulin.InsulinManager; +import com.eveningoutpost.dexdrip.insulin.MultipleInsulins; +import com.eveningoutpost.dexdrip.watch.thinjam.BlueJayEntry; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; +import com.google.gson.internal.bind.DateTypeAdapter; +import com.google.gson.reflect.TypeToken; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; + +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.HOUR_IN_MS; +import static com.eveningoutpost.dexdrip.UtilityModels.Constants.MINUTE_IN_MS; +import static java.lang.StrictMath.abs; +import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; + +// TODO Switchable Carb models +// TODO Linear array timeline optimization + +@Table(name = "Treatments", id = BaseColumns._ID) +public class Treatments extends Model { + private static final String TAG = "jamorham " + Treatments.class.getSimpleName(); + private static final String DEFAULT_EVENT_TYPE = ""; + public final static String XDRIP_TAG = "xdrip"; + + //public static double activityMultipler = 8.4; // somewhere between 8.2 and 8.8 + private static Treatments lastCarbs; + private static boolean patched = false; + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; + @Expose + @Column(name = "eventType") + public String eventType; + @Expose + @Column(name = "enteredBy") + public String enteredBy; + @Expose + @Column(name = "notes") + public String notes; + @Expose + @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public String uuid; + @Expose + @Column(name = "carbs") + public double carbs; + @Expose + @Column(name = "insulin") + public double insulin; + @Expose + @Column(name = "insulinJSON") + public String insulinJSON; + @Expose + @Column(name = "created_at") + public String created_at; + + // don't access this directly use getInsulinInjections() + private List insulinInjections = null; + + private boolean hasInsulinInjections() { + final List injections = getInsulinInjections(); + return ((injections != null) && (injections.size() > 0)); + } + + public boolean isBasalOnly() { + if (!hasInsulinInjections()) return false; + boolean foundBasal = false; + final List injections = getInsulinInjections(); + for (InsulinInjection injection : injections) { + Log.d(TAG,"isBasalOnly: "+injection.isBasal()+" "+injection.getInsulin()); + if (!injection.isBasal()) { + return false; + } else { + foundBasal = true; + } + } + return foundBasal; + } + + private String getInsulinInjectionsShortString() { + final StringBuilder sb = new StringBuilder(); + for (InsulinInjection injection : insulinInjections) { + sb.append(injection.getProfile().getName()); + sb.append(" "); + sb.append(injection.getUnits() + "U "); + } + return sb.toString(); + } + + private void setInsulinInjections(List i) + { + // TODO possiblity here to preserve null if Multiple Injections is not enabled + if (i == null) { + i = new ArrayList<>(); + } + insulinInjections = i; + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + // .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + insulinJSON = gson.toJson(i); + } + + // lazily populate and return InsulinInjection array from json + List getInsulinInjections() { + // Log.d(TAG,"get injections: "+insulinJSON); + if (insulinInjections == null) { + if (insulinJSON != null) { + try { + + insulinInjections = new Gson().fromJson(insulinJSON, new TypeToken>() { + }.getType()); + + StringBuilder x = new StringBuilder(); + for (InsulinInjection y : insulinInjections) { + x.append(y.getProfile().getName() + " " + y.getUnits()+" "); + } + + + } catch (Exception e) { + if (JoH.ratelimit("ij-json-error", 60)) { + UserError.Log.wtf(TAG, "Error converting insulinJson: " + e + " " + insulinJSON); + } + notes = "CORRUPT DATA"; + // state of insulinInjections is basically undefined here as we cannot recover from corrupt data + // we could neutralise the treatment data in other ways perhaps. + } + } else { + // return empty if not set // TODO do we want to cache this or not to avoid memory creation? + return new ArrayList<>(); + } + } + return insulinInjections; + } + + // take a simple insulin value and produce a list assuming it is bolus insulin - for legacy conversion + static private List convertLegacyDoseToBolusInjectionList(final double insulinSum) { + final ArrayList injections = new ArrayList<>(); + injections.add(new InsulinInjection(InsulinManager.getBolusProfile(), insulinSum)); + return injections; + } + + // take a simple insulin value and produce a list by name of insulin - for general quick conversion + public static List convertLegacyDoseToInjectionListByName(final String insulinName, final double insulinSum) { + Log.d(TAG,"convertingLegacyDoseByName: "+insulinName+" "+insulinSum); + final Insulin insulin = InsulinManager.getProfile(insulinName); + if (insulin == null) return null; // TODO should we actually throw an exception here as this should never happen and the result would be invalid + final ArrayList injections = new ArrayList<>(); + injections.add(new InsulinInjection(insulin, insulinSum)); + return injections; + } + + + public void setInsulinJSON(String json) { + if ((json == null) || json.isEmpty()) + json = "[]"; + try { + insulinInjections = new Gson().fromJson(json, new TypeToken>() { + }.getType()); + insulinJSON = json; // set json only if we didn't get exception processing it + } catch (Exception e) { + UserError.Log.e(TAG, "Got exception in setInsulinJson: " + e + " for " + json); + } + } + + public Treatments() + { + eventType = DEFAULT_EVENT_TYPE; + carbs = 0; + insulin = 0; + //setInsulinInjections(null); + } + + public static synchronized Treatments create(final double carbs, final double insulin, long timestamp) { + return create(carbs, insulin, timestamp, null); + } + + public static synchronized Treatments create(final double carbs, final double insulinSum, final long timestamp, final String suggested_uuid) { + + if (MultipleInsulins.isEnabled()) { + return create(carbs, insulinSum, convertLegacyDoseToBolusInjectionList(insulinSum), timestamp, suggested_uuid); + } else { + return create(carbs, insulinSum, null, timestamp, suggested_uuid); + } + + } + + public static synchronized Treatments create(final double carbs, final double insulinSum, final List insulin, long timestamp) { + return create(carbs, insulinSum, insulin, timestamp, null); + } + + public static synchronized Treatments create(final double carbs, final double insulinSum, final List insulin, long timestamp, String suggested_uuid) { + // if treatment more than 1 minutes in the future + final long future_seconds = (timestamp - JoH.tsl()) / 1000; + if (future_seconds > (60 * 60)) { + JoH.static_toast_long("Refusing to create a treatement more than 1 hours in the future!"); + return null; + } + if ((future_seconds > 60) && (future_seconds < 86400) && ((carbs > 0) || (insulinSum > 0))) { + final Context context = xdrip.getAppContext(); + JoH.scheduleNotification(context, "Treatment Reminder", "@" + JoH.hourMinuteString(timestamp) + " : " + + carbs + " " + context.getString(R.string.carbs) + " / " + + insulinSum + " " + context.getString(R.string.units), (int) future_seconds, 34026); + } + return create(carbs, insulinSum, insulin, timestamp, -1, suggested_uuid); + } + + public static synchronized Treatments create(final double carbs, final double insulinSum, final List insulin, long timestamp, double position, String suggested_uuid) { + // TODO sanity check values + Log.d(TAG, "Creating treatment: " + + "Insulin: " + insulinSum + " / " + + "Carbs: " + carbs + + (suggested_uuid != null && !suggested_uuid.isEmpty() + ? " " + "uuid: " + suggested_uuid + : "")); + + if ((carbs == 0) && (insulinSum == 0)) return null; + + if (timestamp == 0) { + timestamp = new Date().getTime(); + } + + final Treatments treatment = new Treatments(); + + if (position > 0) { + treatment.enteredBy = XDRIP_TAG + " pos:" + JoH.qs(position, 2); + } else { + treatment.enteredBy = XDRIP_TAG; + } + + treatment.carbs = carbs; + treatment.insulin = insulinSum; + treatment.setInsulinInjections(insulin); + treatment.timestamp = timestamp; + treatment.created_at = DateUtil.toISOString(timestamp); + treatment.uuid = suggested_uuid != null ? suggested_uuid : UUID.randomUUID().toString(); + treatment.save(); + // GcmActivity.pushTreatmentAsync(Treatment); + // NSClientChat.pushTreatmentAsync(Treatment); + + pushTreatmentSync(treatment); + UndoRedo.addUndoTreatment(treatment.uuid); + return treatment; + } + + // Note + public static synchronized Treatments create_note(String note, long timestamp) { + return create_note(note, timestamp, -1, null); + } + + public static synchronized Treatments create_note(String note, long timestamp, double position) { + return create_note(note, timestamp, position, null); + } + + public static synchronized Treatments create_note(String note, long timestamp, double position, String suggested_uuid) { + // TODO sanity check values + Log.d(TAG, "Creating treatment note: " + note); + + if (timestamp == 0) { + timestamp = new Date().getTime(); + } + + if ((note == null || (note.length() == 0))) { + Log.i(TAG, "Empty treatment note - not saving"); + return null; + } + + boolean is_new = false; + // find treatment + + Treatments treatment = byTimestamp(timestamp, MINUTE_IN_MS * 5); + // if unknown create + if (treatment == null) { + treatment = new Treatments(); + Log.d(TAG, "Creating new treatment entry for note"); + is_new = true; + + treatment.notes = note; + treatment.timestamp = timestamp; + treatment.created_at = DateUtil.toISOString(timestamp); + treatment.uuid = suggested_uuid != null ? suggested_uuid : UUID.randomUUID().toString(); + + } else { + if (treatment.notes == null) treatment.notes = ""; + Log.d(TAG, "Found existing treatment for note: " + treatment.uuid + ((suggested_uuid != null) ? " vs suggested: " + suggested_uuid : "") + " distance:" + Long.toString(timestamp - treatment.timestamp) + " " + treatment.notes); + if (treatment.notes.contains(note)) { + Log.d(TAG, "Suggested note update already present - skipping"); + return null; + } + // append existing note or treatment + if (treatment.notes.length() > 0) treatment.notes += " \u2192 "; + treatment.notes += note; + Log.d(TAG, "Final notes: " + treatment.notes); + } + // if ((treatment.enteredBy == null) || (!treatment.enteredBy.contains(NightscoutUploader.VIA_NIGHTSCOUT_TAG))) { + // tag it as from xdrip if it isn't being synced from nightscout right now to allow local updates to nightscout sourced notes + if (suggested_uuid == null) { + if (position > 0) { + treatment.enteredBy = XDRIP_TAG + " pos:" + JoH.qs(position, 2); + } else { + treatment.enteredBy = XDRIP_TAG; + } + } + + treatment.save(); + + pushTreatmentSync(treatment, is_new, suggested_uuid); + if (is_new) UndoRedo.addUndoTreatment(treatment.uuid); + + return treatment; + } + + public static synchronized Treatments SensorStart(long timestamp) { + if (timestamp == 0) { + timestamp = new Date().getTime(); + } + + final Treatments Treatment = new Treatments(); + Treatment.enteredBy = XDRIP_TAG; + Treatment.eventType = "Sensor Start"; + Treatment.created_at = DateUtil.toISOString(timestamp); + Treatment.timestamp = timestamp; + Treatment.uuid = UUID.randomUUID().toString(); + Treatment.save(); + pushTreatmentSync(Treatment); + return Treatment; + } + + private static void pushTreatmentSync(Treatments treatment) { + pushTreatmentSync(treatment, true, null); // new entry by default + } + + private static void pushTreatmentSync(Treatments treatment, boolean is_new, String suggested_uuid) { + + if (Home.get_master_or_follower()) GcmActivity.pushTreatmentAsync(treatment); + + if (!(Pref.getBoolean("cloud_storage_api_enable", false) || Pref.getBoolean("cloud_storage_mongodb_enable", false))) { + NSClientChat.pushTreatmentAsync(treatment); + } else { + Log.d(TAG, "Skipping NSClient treatment broadcast as nightscout direct sync is enabled"); + } + + if (suggested_uuid == null) { + // only sync to nightscout if source of change was not from nightscout + if (UploaderQueue.newEntry(is_new ? "insert" : "update", treatment) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + + public static void pushTreatmentSyncToWatch(Treatments treatment, boolean is_new) { + Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); + if (Pref.getBooleanDefaultFalse("wear_sync")) { + if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", treatment) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + + // This shouldn't be needed but it seems it is + private static void fixUpTable() { + if (patched) return; + String[] patchup = { + "CREATE TABLE Treatments (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE Treatments ADD COLUMN timestamp INTEGER;", + "ALTER TABLE Treatments ADD COLUMN uuid TEXT;", + "ALTER TABLE Treatments ADD COLUMN eventType TEXT;", + "ALTER TABLE Treatments ADD COLUMN enteredBy TEXT;", + "ALTER TABLE Treatments ADD COLUMN notes TEXT;", + "ALTER TABLE Treatments ADD COLUMN created_at TEXT;", + "ALTER TABLE Treatments ADD COLUMN insulin REAL;", + "ALTER TABLE Treatments ADD COLUMN insulinJSON TEXT;", + "ALTER TABLE Treatments ADD COLUMN carbs REAL;", + "CREATE INDEX index_Treatments_timestamp on Treatments(timestamp);", + "CREATE UNIQUE INDEX index_Treatments_uuid on Treatments(uuid);"}; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + //Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } + + public static Treatments last() { + fixUpTable(); + return new Select() + .from(Treatments.class) + .orderBy("_ID desc") + .executeSingle(); + } + + public static Treatments lastNotFromXdrip() { + fixUpTable(); + return new Select() + .from(Treatments.class) + .where("enteredBy NOT LIKE '" + XDRIP_TAG + "%'") + .orderBy("_ID DESC") + .executeSingle(); + } + + public static List latest(int num) { + try { + return new Select() + .from(Treatments.class) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static Treatments byuuid(String uuid) { + if (uuid == null) return null; + return new Select() + .from(Treatments.class) + .where("uuid = ?", uuid) + .orderBy("_ID desc") + .executeSingle(); + } + + public static Treatments byid(long id) { + return new Select() + .from(Treatments.class) + .where("_ID = ?", id) + .executeSingle(); + } + + public static Treatments byTimestamp(long timestamp) { + return byTimestamp(timestamp, 1500); + } + + public static Treatments byTimestamp(long timestamp, long plus_minus_millis) { + if (plus_minus_millis > Integer.MAX_VALUE) { + throw new RuntimeException("Treatment by TimeStamp out of range value: " + plus_minus_millis); + } + return byTimestamp(timestamp, (int) plus_minus_millis); + } + + public static Treatments byTimestamp(long timestamp, int plus_minus_millis) { + return new Select() + .from(Treatments.class) + .where("timestamp <= ? and timestamp >= ?", (timestamp + plus_minus_millis), (timestamp - plus_minus_millis)) // window + .orderBy("abs(timestamp-" + Long.toString(timestamp) + ") asc") + .executeSingle(); + } + + public static void delete_all() { + delete_all(false); + } + + public static void delete_all(boolean from_interactive) { + if (from_interactive) { + GcmActivity.push_delete_all_treatments(); + } + new Delete() + .from(Treatments.class) + .execute(); + // not synced with uploader queue - should we? + } + + public static Treatments delete_last() { + return delete_last(false); + } + + public static void delete_by_timestamp(long timestamp) { + delete_by_timestamp(timestamp, 1500, false); + } + + public static void delete_by_timestamp(long timestamp, int accuracy, boolean from_interactive) { + final Treatments t = byTimestamp(timestamp, accuracy); // do we need to alter default accuracy? + if (t != null) { + Log.d(TAG, "Deleting treatment closest to: " + JoH.dateTimeText(timestamp) + " matches uuid: " + t.uuid); + delete_by_uuid(t.uuid, from_interactive); + } else { + Log.e(TAG, "Couldn't find a treatment near enough to " + JoH.dateTimeText(timestamp) + " to delete!"); + } + } + + public static void delete_by_uuid(String uuid) { + delete_by_uuid(uuid, false); + } + + public static void delete_by_uuid(String uuid, boolean from_interactive) { + Treatments thistreat = byuuid(uuid); + if (thistreat != null) { + + UploaderQueue.newEntry("delete", thistreat); + if (from_interactive) { + GcmActivity.push_delete_treatment(thistreat); + SyncService.startSyncService(3000); // sync in 3 seconds + } + + thistreat.delete(); + Home.staticRefreshBGCharts(); + } + } + + public static Treatments delete_last(boolean from_interactive) { + Treatments thistreat = last(); + if (thistreat != null) { + + if (from_interactive) { + GcmActivity.push_delete_treatment(thistreat); + //GoogleDriveInterface gdrive = new GoogleDriveInterface(); + //gdrive.deleteTreatmentAtRemote(thistreat.uuid); + } + UploaderQueue.newEntry("delete", thistreat); + thistreat.delete(); + } + return null; + } + + public static Treatments fromJSON(String json) { + try { + return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, Treatments.class); + } catch (Exception e) { + Log.d(TAG, "Got exception parsing treatment json: " + e.toString()); + Home.toaststatic("Error on treatment, probably decryption key mismatch"); + return null; + } + } + + public static synchronized boolean pushTreatmentFromJson(String json) { + return pushTreatmentFromJson(json, false); + } + + public static synchronized boolean pushTreatmentFromJson(String json, boolean from_interactive) { + Log.d(TAG, "converting treatment from json: " + json); + final Treatments mytreatment = fromJSON(json); + if (mytreatment != null) { + if ((mytreatment.carbs == 0) && (mytreatment.insulin == 0) + && (mytreatment.notes != null) && (mytreatment.notes.startsWith("AndroidAPS started"))) { + Log.d(TAG, "Skipping AndroidAPS started message"); + return false; + } + if ((mytreatment.eventType != null) && (mytreatment.eventType.equals("Temp Basal"))) { + // we don't yet parse or process these + Log.d(TAG, "Skipping Temp Basal msg"); + return false; + } + + if (mytreatment.uuid == null) { + try { + final JSONObject jsonobj = new JSONObject(json); + if (jsonobj.has("_id")) mytreatment.uuid = jsonobj.getString("_id"); + } catch (JSONException e) { + // + } + if (mytreatment.uuid == null) mytreatment.uuid = UUID.randomUUID().toString(); + } + // anything received +- 1500 ms is going to be treated as a duplicate + final Treatments dupe_treatment = byTimestamp(mytreatment.timestamp); + if (dupe_treatment != null) { + Log.i(TAG, "Duplicate treatment for: " + mytreatment.timestamp); + + if ((dupe_treatment.insulin == 0) && (mytreatment.insulin > 0)) { + dupe_treatment.setInsulinJSON(mytreatment.insulinJSON); + dupe_treatment.insulin = mytreatment.insulin; + dupe_treatment.save(); + Home.staticRefreshBGChartsOnIdle(); + } + + if ((dupe_treatment.carbs == 0) && (mytreatment.carbs > 0)) { + dupe_treatment.carbs = mytreatment.carbs; + dupe_treatment.save(); + Home.staticRefreshBGChartsOnIdle(); + } + + if ((dupe_treatment.uuid != null) && (mytreatment.uuid != null) && (dupe_treatment.uuid.equals(mytreatment.uuid)) && (mytreatment.notes != null)) { + + if ((dupe_treatment.notes == null) || (dupe_treatment.notes.length() < mytreatment.notes.length())) { + dupe_treatment.notes = mytreatment.notes; + fixUpTable(); + dupe_treatment.save(); + Log.d(TAG, "Saved updated treatement notes"); + // should not end up needing to append notes and be from_interactive via undo as these + // would be mutually exclusive operations so we don't need to handle that here. + Home.staticRefreshBGChartsOnIdle(); + // TODO review if this is correct place for new notes only + evaluateNotesForNotification(mytreatment); + } + } + + return false; + } + Log.d(TAG, "Saving pushed treatment: " + mytreatment.uuid); + if ((mytreatment.enteredBy == null) || (mytreatment.enteredBy.equals(""))) { + mytreatment.enteredBy = "sync"; + } + if ((mytreatment.eventType == null) || (mytreatment.eventType.equals(""))) { + mytreatment.eventType = DEFAULT_EVENT_TYPE; // should have a default + } + if ((mytreatment.created_at == null) || (mytreatment.created_at.equals(""))) { + try { + mytreatment.created_at = DateUtil.toISOString(mytreatment.timestamp); // should have a default + } catch (Exception e) { + Log.e(TAG, "Could not convert timestamp to isostring"); + } + } + + fixUpTable(); + long x = mytreatment.save(); + Log.d(TAG, "Saving treatment result: " + x); + if (from_interactive) { + pushTreatmentSync(mytreatment); + } + // TODO review if this is correct place for new notes only + evaluateNotesForNotification(mytreatment); + Home.staticRefreshBGChartsOnIdle(); + return true; + } else { + return false; + } + } + + private static void evaluateNotesForNotification(final Treatments mytreatment) { + if (!emptyString(mytreatment.notes) && mytreatment.notes.startsWith("-")) { + BlueJayEntry.sendNotifyIfEnabled(mytreatment.notes); + } + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, startTime, JoH.ts()); + } + + public static List latestForGraph(int number, double startTime, double endTime) { + fixUpTable(); + DecimalFormat df = new DecimalFormat("#"); + df.setMaximumFractionDigits(1); // are there decimal points in the database?? + return new Select() + .from(Treatments.class) + .where("timestamp >= ? and timestamp <= ?", df.format(startTime), df.format(endTime)) + .orderBy("timestamp asc") + .limit(number) + .execute(); + } + + public static List latestForGraph(final int number, final long startTime, final long endTime) { + fixUpTable(); + return new Select() + .from(Treatments.class) + .where("timestamp >= ? and timestamp <= ?", startTime, endTime) + .orderBy("timestamp asc") + .limit(number) + .execute(); + } + + public static long getTimeStampWithOffset(double offset) { + // optimisation instead of creating a new date each time? + return (long) (new Date().getTime() - offset); + } + + /// this is no longer used + /* public static CobCalc cobCalc(Treatments treatment, double lastDecayedBy, double time) { + + double delay = 20; // minutes till carbs start decaying + + double delayms = delay * Constants.MINUTE_IN_MS; + if (treatment.carbs > 0) { + + CobCalc thisCobCalc = new CobCalc(); + thisCobCalc.carbTime = treatment.timestamp; + + // no previous carb treatment? Set to our start time + if (lastDecayedBy == 0) { + lastDecayedBy = thisCobCalc.carbTime; + } + + double carbs_hr = Profile.getCarbAbsorptionRate(time); + double carbs_min = carbs_hr / 60; + double carbs_ms = carbs_min / Constants.MINUTE_IN_MS; + + thisCobCalc.decayedBy = thisCobCalc.carbTime; // initially set to start time for this treatment + + double minutesleft = (lastDecayedBy - thisCobCalc.carbTime) / Constants.MINUTE_IN_MS; + double how_long_till_carbs_start_ms = (lastDecayedBy - thisCobCalc.carbTime); + thisCobCalc.decayedBy += (Math.max(delay, minutesleft) + treatment.carbs / carbs_min) * Constants.MINUTE_IN_MS; + + if (delay > minutesleft) { + thisCobCalc.initialCarbs = treatment.carbs; + } else { + thisCobCalc.initialCarbs = treatment.carbs + minutesleft * carbs_min; + } + double startDecay = thisCobCalc.carbTime + (delay * Constants.MINUTE_IN_MS); + + if (time < lastDecayedBy || time > startDecay) { + thisCobCalc.isDecaying = 1; + } else { + thisCobCalc.isDecaying = 0; + } + return thisCobCalc; + + } else { + return null; + } + } +*/ + + // when using multiple insulins + private static Pair calculateIobActivityFromTreatmentAtTime(final Treatments treatment, final double time, final boolean useBasal) { + + double iobContrib = 0, activityContrib = 0; + if (treatment.insulin > 0) { + // Log.d(TAG,"NEW TYPE insulin: "+treatment.insulin+ " "+treatment.insulinJSON); + // translate a legacy entry to be bolus insulin + List injectionsList = treatment.getInsulinInjections(); + if (injectionsList == null || injectionsList.size() == 0) { + Log.d(TAG,"CONVERTING LEGACY: "+treatment.insulinJSON+ " "+injectionsList); + injectionsList = convertLegacyDoseToBolusInjectionList(treatment.insulin); + treatment.insulinInjections = injectionsList; // cache but best not to save it + } + + for (final InsulinInjection injection : injectionsList) + if (injection.getUnits() > 0 && (useBasal || !injection.isBasal())) { + iobContrib += injection.getUnits() * abs(injection.getProfile().calculateIOB((time - treatment.timestamp) / MINUTE_IN_MS)); + activityContrib += injection.getUnits() * abs(injection.getProfile().calculateActivity((time - treatment.timestamp) / MINUTE_IN_MS)); + } + if (iobContrib < 0) iobContrib = 0; + if (activityContrib < 0) activityContrib = 0; + } + return new Pair<>(iobContrib, activityContrib); + } + + // using the original calculation + private static Pair calculateLegacyIobActivityFromTreatmentAtTime(final Treatments treatment, final double time) { + + final double dia = Profile.insulinActionTime(time); // duration insulin action in hours + final double peak = 75; // minutes in based on a 3 hour DIA - scaled proportionally (orig 75) + + double insulin_delay_minutes = 0; + + double insulin_timestamp = treatment.timestamp + (insulin_delay_minutes * 60 * 1000); + + //Iob response = new Iob(); + + final double scaleFactor = 3.0 / dia; + double iobContrib = 0; + //double activityContrib = 0; + + // only use treatments with insulin component which have already happened + if ((treatment.insulin > 0) && (insulin_timestamp < time)) { + // double bolusTime = insulin_timestamp; // bit of a dupe + double minAgo = scaleFactor * (((time - insulin_timestamp) / 1000) / 60); + + if (minAgo < peak) { + double x1 = minAgo / 5 + 1; + iobContrib = treatment.insulin * (1 - 0.001852 * x1 * x1 + 0.001852 * x1); + // units: BG (mg/dL) = (BG/U) * U insulin * scalar + // activityContrib = sens * activityMultipler * treatment.insulin * (2 / dia / 60 / peak) * minAgo; + + } else if (minAgo < 180) { + double x2 = (minAgo - peak) / 5; + iobContrib = treatment.insulin * (0.001323 * x2 * x2 - .054233 * x2 + .55556); + // activityContrib = sens * activityMultipler * treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * dia - peak)); + } + + } + if (iobContrib < 0) iobContrib = 0; + //if (activityContrib < 0) activityContrib = 0; + return new Pair<>(iobContrib, 0d); + } + + + + private static Iob calcTreatment(final Treatments treatment, final double time, final boolean useBasal) { + final Iob response = new Iob(); + + if (MultipleInsulins.isEnabled()) { + Pair result = calculateIobActivityFromTreatmentAtTime(treatment, time, useBasal); + response.iob = result.first; + response.jActivity = result.second; + } else { + Pair result = calculateLegacyIobActivityFromTreatmentAtTime(treatment, time); + response.iob = result.first; + // response.jActivity = result.second; + } + + return response; + } + + // requires stepms granularity which we should already have + private static double timesliceIactivityAtTime(Map timeslices, double thistime) { + if (timeslices.containsKey(thistime)) { + return timeslices.get(thistime).jActivity; + } else { + return 0; + } + } + + private static void timesliceCarbWriter(Map timeslices, double thistime, double carbs) { + // offset for carb action time?? + Iob tempiob; + if (timeslices.containsKey(thistime)) { + tempiob = timeslices.get(thistime); + tempiob.cob = tempiob.cob + carbs; + } else { + tempiob = new Iob(); + tempiob.timestamp = (long) thistime; + // tempiob.date = new Date((long)thistime); + tempiob.cob = carbs; + } + timeslices.put(thistime, tempiob); + } + + private static void timesliceInsulinWriter(Map timeslices, Iob thisiob, double thistime) { + if (thisiob.iob > 0) { + if (timeslices.containsKey(thistime)) { + Iob tempiob = timeslices.get(thistime); + tempiob.iob += thisiob.iob; + tempiob.jActivity+= thisiob.jActivity; + timeslices.put(thistime, tempiob); + } else { + thisiob.timestamp = (long) thistime; + // thisiob.date = new Date((long)thistime); + timeslices.put(thistime, thisiob); // first entry at timeslice so put the record in as is + } + } + } + + // NEW NEW NEW + public static List ioBForGraph_new(int number, double startTime) { + + // Log.d(TAG, "Processing iobforgraph2: main "); + JoH.benchmark_method_start(); + final boolean multipleInsulins = MultipleInsulins.isEnabled(); + final boolean useBasal = MultipleInsulins.useBasalActivity(); + // number param currently ignored + + // 10 hours max look or from insulin manager if enabled + final double dontLookThisFar = MultipleInsulins.isEnabled() ? MINUTE_IN_MS * InsulinManager.getMaxEffect(true) : 10 * HOUR_IN_MS; +// look back the longest effect period of all enabled insulin profiles (startTime is always 24h behind NOW) + List theTreatments = latestForGraph(2000, startTime - dontLookThisFar); + Log.d(TAG,"TREATMENT LIST: "+theTreatments.size()+" "+JoH.dateTimeText((long)(startTime - dontLookThisFar))); + if (theTreatments.size() == 0) return null; + + int counter = 0; // iteration counter + + final double step_minutes = 5; + final double stepms = step_minutes * MINUTE_IN_MS; // 300s = 5 mins + double mytime = startTime; + double tendtime = startTime; + + + final double carb_delay_minutes = Profile.carbDelayMinutes(mytime); // not likely a time dependent parameter + final double carb_delay_ms_stepped = ((long) (carb_delay_minutes / step_minutes)) * step_minutes * MINUTE_IN_MS; + + Log.d(TAG, "Carb delay ms: " + carb_delay_ms_stepped); + + Map carbsEaten = new HashMap(); + + // linear array populated as needed and layered by each treatment etc + SortedMap timeslices = new TreeMap(); + Iob calcreply; + + // First process all IoB calculations + for (Treatments thisTreatment : theTreatments) { + // early optimisation exclusion + + mytime = ((long) (thisTreatment.timestamp / stepms)) * stepms; // effects of treatment occur only after it is given / fit to slot time + tendtime = mytime + 36 * HOUR_IN_MS; // 36 hours max look (24h history plus 12h forecast) + if (tendtime > startTime + 30 * HOUR_IN_MS) + tendtime = startTime + 30 * HOUR_IN_MS; // dont look more than 6h in future // TODO review time limit + if (thisTreatment.insulin > 0) { + // lay down insulin on board + do { + + calcreply = calcTreatment(thisTreatment, mytime, useBasal); + calcreply.jActivity *= step_minutes; // has to be multiplied because derivation function of IOB calculates a step_minutes lower activity as the "old" logic + calcreply.jActivity *= Profile.getSensitivity(mytime); + + if (mytime >= startTime) { + timesliceInsulinWriter(timeslices, calcreply, mytime); + } + mytime = mytime + stepms; // advance time counter + } while ((mytime < tendtime) && + ((calcreply.iob == 0) || (calcreply.iob > 0.01))); + } + } // per insulin treatment + + // legacy jActivity calculation + if (!multipleInsulins) { + Log.d(TAG, "Single insulin type iteration counter: " + counter); + + // evaluate insulin impact + Iob lastiob = null; + for (Map.Entry entry : timeslices.entrySet()) { + Iob thisiob = entry.getValue(); + if (lastiob != null) { + if ((thisiob.iob != 0) || (lastiob.iob != 0)) { + if (thisiob.iob < lastiob.iob) { + // decaying iob + thisiob.jActivity = (lastiob.iob - thisiob.iob) * Profile.getSensitivity(thisiob.timestamp); + } else { + // more insulin added + thisiob.jActivity = 0; // TODO THIS IS NOT RIGHT IT MISSES ONE DECAY STEP + } + } + } + + //Log.d(TAG,"iobinfo2 iob debug: "+JoH.qs(thisiob.timestamp)+" C:"+JoH.qs(thisiob.cob,4)+" I:"+JoH.qs(thisiob.iob,4)+" CA:"+JoH.qs(thisiob.jCarbImpact)+" IA:"+JoH.qs(thisiob.jActivity)); + counter++; + lastiob = thisiob; + } + // + } + + // calculate carb treatments + for (Treatments thisTreatment : theTreatments) { + + if (thisTreatment.carbs > 0) { + + mytime = ((long) (thisTreatment.timestamp / stepms)) * stepms; // effects of treatment occur only after it is given / fit to slot time + tendtime = mytime + 6 * HOUR_IN_MS; // 6 hours max look + + double cob_time = mytime + carb_delay_ms_stepped; + double stomachDiff = ((Profile.getCarbAbsorptionRate(cob_time) * stepms) / HOUR_IN_MS); // initial value + double newdelayedCarbs = 0; + double cob_remain = thisTreatment.carbs; + while ((cob_remain > 0) && (stomachDiff > 0) && (cob_time < tendtime)) { + + if (cob_time >= startTime) { + timesliceCarbWriter(timeslices, cob_time, cob_remain); + } + cob_time += stepms; + + stomachDiff = ((Profile.getCarbAbsorptionRate(cob_time) * stepms) / HOUR_IN_MS); + cob_remain -= stomachDiff; + + newdelayedCarbs = (timesliceIactivityAtTime(timeslices, cob_time) * Profile.getLiverSensRatio(cob_time) / Profile.getSensitivity(cob_time)) * Profile.getCarbRatio(cob_time); + + if (newdelayedCarbs > 0) { + final double maximpact = stomachDiff * Profile.maxLiverImpactRatio(cob_time); + if (newdelayedCarbs > maximpact) newdelayedCarbs = maximpact; + cob_remain += newdelayedCarbs; // add back on liverfactor adjustment + } + + counter++; + + } + // end record if not present + if (cob_time >= startTime) { + timesliceCarbWriter(timeslices, cob_time, 0); + } + } + } + + // evaluate carb impact + Iob lastiob = null; + for (Map.Entry entry : timeslices.entrySet()) { + Iob thisiob = entry.getValue(); + if (lastiob != null) { + if ((thisiob.cob != 0 || (lastiob.cob != 0))) { + if (thisiob.cob < lastiob.cob) { + // decaying cob + thisiob.jCarbImpact = (lastiob.cob - thisiob.cob) / Profile.getCarbRatio(thisiob.timestamp) * Profile.getSensitivity(thisiob.timestamp); + } else { + // more carbs added + thisiob.jCarbImpact = 0; // TODO THIS IS NOT RIGHT IT MISSES ONE DECAY STEP + } + } + } + + // Log.d(TAG,"iobinfo2carb debug: "+JoH.qs(thisiob.timestamp)+" C:"+JoH.qs(thisiob.cob,4)+" I:"+JoH.qs(thisiob.iob,4)+" CA:"+JoH.qs(thisiob.jCarbImpact)+" IA:"+JoH.qs(thisiob.jActivity)); + counter++; + lastiob = thisiob; + } + + Log.d(TAG, "second iteration counter: " + counter); + Log.d(TAG, "Timeslices size: " + timeslices.size()); + JoH.benchmark_method_end(); + return new ArrayList(timeslices.values()); + } + + + /* /// OLD ONE BELOW + + public static List ioBForGraph_old(int number, double startTime) { + + JoH.benchmark_method_start(); + //JoH.benchmark_method_end(); + + Log.d(TAG, "Processing iobforgraph: main "); + // get all treatments from 24 hours earlier than our current time + List theTreatments = latestForGraph(2000, startTime - 86400000); + Map carbsEaten = new HashMap(); + // this could be much more optimized with linear array instead of loops + + final double dontLookThisFar = 10 * 60 * 60 * 1000; // 10 hours max look + + double stomachCarbs = 0; + + final double step_minutes = 10; + final double stepms = step_minutes * 60 * 1000; // 600s = 10 mins + + if (theTreatments.size() == 0) return null; + + Map ioblookup = new HashMap(); // store for iob total vs time + + List responses = new ArrayList(); + Iob calcreply; + + double mytime = startTime; + double lastmytime = mytime; + double max_look_time = startTime + (30 * 60 * 60 * 1000); + int counter = 0; + // 30 hours max look at + while ((responses.size() < number) && (mytime < max_look_time)) { + + double lastDecayedBy = 0, isDecaying = 0, delayMinutes = 0; // reset per time slot + double totalIOB = 0, totalCOB = 0, totalActivity = 0; + // per treatment per timeblock + for (Treatments thisTreatment : theTreatments) { + // early optimisation exclusion + if ((thisTreatment.timestamp <= mytime) && (mytime - thisTreatment.timestamp) < dontLookThisFar) { + calcreply = calcTreatment(thisTreatment, mytime, lastDecayedBy); // was last decayed by but that offset wrongly?? + totalIOB += calcreply.iob; + //totalCOB += calcreply.cob; + totalActivity += calcreply.activity; + } // endif excluding a treatment + } // per treatment + + // + ioblookup.put(mytime, totalIOB); + if (ioblookup.containsKey(lastmytime)) { + double iobdiff = (double) ioblookup.get(lastmytime) - totalIOB; + if (iobdiff < 0) iobdiff = 0; + if ((iobdiff != 0) || (totalActivity != 0)) { + Log.d(TAG, "New IOB diffi @: " + JoH.qs(mytime) + " = " + JoH.qs(iobdiff) + " old activity: " + JoH.qs(totalActivity)); + } + totalActivity = iobdiff; // WARNING OVERRIDE + } + + double stomachDiff = ((Profile.getCarbAbsorptionRate(mytime) * stepms) / (60 * 60 * 1000)); + double newdelayedCarbs = (totalActivity * Profile.getLiverSensRatio(mytime) / Profile.getSensitivity(mytime)) * Profile.getCarbRatio(mytime); + + // calculate carbs + for (Treatments thisTreatment : theTreatments) { + // early optimisation exclusion + if ((thisTreatment.timestamp <= mytime) && (mytime - thisTreatment.timestamp) < dontLookThisFar) { + if ((thisTreatment.carbs > 0) && (thisTreatment.timestamp < mytime)) { + // factor carbs delay in above when complete + if (!carbsEaten.containsKey(thisTreatment.uuid)) { + carbsEaten.put(thisTreatment.uuid, true); + stomachCarbs = stomachCarbs + thisTreatment.carbs; + stomachCarbs = stomachCarbs + stomachDiff; // offset first subtraction + // pre-subtract for granularity or just reduce granularity + Log.d(TAG, "newcarbs: " + thisTreatment.carbs + " " + thisTreatment.uuid + " @ " + thisTreatment.timestamp + " mytime: " + JoH.qs(mytime) + " diff: " + JoH.qs((thisTreatment.timestamp - mytime) / 1000) + " stomach: " + JoH.qs(stomachCarbs)); + } + lastCarbs = thisTreatment; + CobCalc cCalc = cobCalc(thisTreatment, lastDecayedBy, mytime); // need to handle last decayedby shunting + double decaysin_hr = (cCalc.decayedBy - mytime) / 1000 / 60 / 60; + if (decaysin_hr > -10) { + // units: BG + double avgActivity = totalActivity; + // units: g = BG * scalar / BG / U * g / U + double delayedCarbs = (avgActivity * Profile.getLiverSensRatio(mytime) / Profile.getSensitivity(mytime)) * Profile.getCarbRatio(mytime); + + delayMinutes = Math.round(delayedCarbs / (Profile.getCarbAbsorptionRate(mytime) / 60)); + Log.d(TAG, "Avg activity: " + JoH.qs(avgActivity) + " Decaysin_hr: " + JoH.qs(decaysin_hr) + " delay minutes: " + JoH.qs(delayMinutes) + " delayed carbs: " + JoH.qs(delayedCarbs)); + if (delayMinutes > 0) { + Log.d(TAG, "Delayed Carbs: " + JoH.qs(delayedCarbs) + " Delay minutes: " + JoH.qs(delayMinutes) + " Average activity: " + JoH.qs(avgActivity)); + cCalc.decayedBy += delayMinutes * 60 * 1000; + decaysin_hr = (cCalc.decayedBy - mytime) / 1000 / 60 / 60; + } + } + + lastDecayedBy = cCalc.decayedBy; + + if (decaysin_hr > 0) { + Log.d(TAG, "cob: Adding " + JoH.qs(delayMinutes) + " minutes to decay of " + JoH.qs(thisTreatment.carbs) + "g bolus at " + JoH.qs(thisTreatment.timestamp)); + totalCOB += Math.min(thisTreatment.carbs, decaysin_hr * Profile.getCarbAbsorptionRate(thisTreatment.timestamp)); + Log.d(TAG, "cob: " + JoH.qs(Math.min(cCalc.initialCarbs, decaysin_hr * Profile.getCarbAbsorptionRate(thisTreatment.timestamp))) + + " inital carbs:" + JoH.qs(cCalc.initialCarbs) + " decaysin_hr:" + JoH.qs(decaysin_hr) + " absorbrate:" + JoH.qs(Profile.getCarbAbsorptionRate(thisTreatment.timestamp))); + isDecaying = cCalc.isDecaying; + } else { + // totalCOB = 0; //nix this? + } + } // if this treatment has carbs + } // end if processing this treatment + } // per carb treatment + + if (stomachCarbs > 0) { + + Log.d(TAG, "newcarbs Stomach Diff: " + JoH.qs(stomachDiff) + " Old total: " + JoH.qs(stomachCarbs) + " Delayed carbs: " + JoH.qs(newdelayedCarbs)); + + stomachCarbs = stomachCarbs - stomachDiff; + if (newdelayedCarbs > 0) { + double maximpact = stomachDiff * Profile.maxLiverImpactRatio(mytime); + if (newdelayedCarbs > maximpact) newdelayedCarbs = maximpact; + stomachCarbs = stomachCarbs + newdelayedCarbs; // add back on liverfactor ones + } + if (stomachCarbs < 0) stomachCarbs = 0; + } + + if ((totalIOB > Profile.minimum_shown_iob) || (totalCOB > Profile.minimum_shown_cob) || (stomachCarbs > Profile.minimum_shown_cob)) { + Iob thisrecord = new Iob(); + + thisrecord.timestamp = (long) mytime; + thisrecord.iob = totalIOB; + thisrecord.activity = totalActivity; // hacky cruft + thisrecord.cob = stomachCarbs; + thisrecord.jCarbImpact = 0; // calculated below + thisrecord.rawCarbImpact = (isDecaying * Profile.getSensitivity(mytime)) / Profile.getCarbRatio(mytime) * Profile.getCarbAbsorptionRate(mytime) / 60; + + // don't get confused with cob totals from previous treatments + if ((responses.size() > 0) && (Math.abs(responses.get(responses.size() - 1).timestamp - thisrecord.timestamp) <= stepms)) { + double cobdiff = responses.get(responses.size() - 1).cob - thisrecord.cob; + if (cobdiff > 0) { + thisrecord.jCarbImpact = (cobdiff / Profile.getCarbRatio(mytime)) * Profile.getSensitivity(mytime); + } + + double iobdiff = responses.get(responses.size() - 1).iob - totalIOB; + if (iobdiff > 0) { + thisrecord.jActivity = (iobdiff * Profile.getSensitivity(mytime)); + } + } + + Log.d(TAG, "added record: cob raw impact: " + Double.toString(thisrecord.rawCarbImpact) + " Isdecaying: " + + JoH.qs(isDecaying) + " jCarbImpact: " + JoH.qs(thisrecord.jCarbImpact) + + " jActivity: " + JoH.qs(thisrecord.jActivity) + " old activity: " + JoH.qs(thisrecord.activity)); + + responses.add(thisrecord); + } + lastmytime = mytime; + mytime = mytime + stepms; + counter++; + } // while time period in range + + Log.d(TAG, "Finished Processing iobforgraph: main - processed: " + Integer.toString(counter) + " Timeslot records"); + JoH.benchmark_method_end(); + return responses; + }*/ + + public String getBestShortText() { + if (!eventType.equals(DEFAULT_EVENT_TYPE)) { + return eventType; + } else { + if (hasInsulinInjections()) { + return getInsulinInjectionsShortString() + + (noteHasContent() ? (" " + notes) : ""); + } else { + return noteHasContent() ? notes : "Treatment"; + } + } + } + + public String toJSON() { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("uuid", uuid); + jsonObject.put("insulin", insulin); + jsonObject.put("insulinJSON", insulinJSON); + jsonObject.put("carbs", carbs); + jsonObject.put("timestamp", timestamp); + jsonObject.put("notes", notes); + jsonObject.put("enteredBy", enteredBy); + return jsonObject.toString(); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return ""; + } + } + + private static final double MAX_SMB_UNITS = 0.3; + private static final double MAX_OPENAPS_SMB_UNITS = 0.4; + + public boolean likelySMB() { + return (carbs == 0 && insulin > 0 + && ((insulin <= MAX_SMB_UNITS && (notes == null || notes.length() == 0)) || (enteredBy != null && enteredBy.startsWith("openaps:") && insulin <= MAX_OPENAPS_SMB_UNITS))); + } + + public boolean noteOnly() { + return carbs == 0 && insulin == 0 && noteHasContent(); + } + + public boolean hasContent() { + return insulin != 0 || carbs != 0 || noteHasContent() || !isEventTypeDefault(); + } + + public boolean noteHasContent() { + return notes != null && notes.length() > 0; + } + + public boolean isEventTypeDefault() { + return eventType == null || eventType.equalsIgnoreCase(DEFAULT_EVENT_TYPE); + } + + public static boolean matchUUID(final List treatments, final String uuid) { + for (final Treatments treatment : treatments) { + if (treatment.uuid.equalsIgnoreCase(uuid)) return true; + } + return false; + } + + public String toS() { + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Date.class, new DateTypeAdapter()) + .serializeSpecialFloatingPointValues() + .create(); + return gson.toJson(this); + } +} + + + diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserError.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserError.java new file mode 100644 index 0000000..a363ad2 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserError.java @@ -0,0 +1,413 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.os.AsyncTask; +import android.provider.BaseColumns; + +import com.activeandroid.Cache; +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.google.gson.annotations.Expose; + +import java.util.Date; +import java.util.Hashtable; +import java.util.List; + +//import com.bugfender.sdk.Bugfender; + +/** + * Created by Emma Black on 8/3/15. + */ + +@Table(name = "UserErrors", id = BaseColumns._ID) +public class UserError extends Model { + + private final static String TAG = UserError.class.getSimpleName(); + + @Expose + @Column(name = "shortError") + public String shortError; // Short error message to be displayed on table + + @Expose + @Column(name = "message") + public String message; // Additional text when error is expanded + + @Expose + @Column(name = "severity", index = true) + public int severity; // int between 1 and 3, 3 being most severe + + // 5 = internal lower level user events + // 6 = higher granularity user events + + @Expose + @Column(name = "timestamp", index = true) + public long timestamp; // Time the error was raised + + //todo: rather than include multiples of the same error, should we have a "Count" and just increase that on duplicates? + //or rather, perhaps we should group up the errors + + public String toString() + { + return severity+" ^ "+JoH.dateTimeText((long)timestamp)+" ^ "+shortError+" ^ "+message; + } + + public UserError() {} + + public UserError(int severity, String shortError, String message) { + this.severity = severity; + this.shortError = shortError; + this.message = message; + this.timestamp = new Date().getTime(); + this.save(); + /* if (xdrip.useBF) { + switch (severity) { + case 2: + case 3: + Bugfender.e(shortError, message); + break; + case 5: + case 6: + Bugfender.w(shortError, message); + break; + default: + Bugfender.d(shortError, message); + break; + } + }*/ + } + + public UserError(String shortError, String message) { + this(2, shortError, message); + } + + public static UserError UserErrorHigh(String shortError, String message) { + return new UserError(3, shortError, message); + } + + public static UserError UserErrorLow(String shortError, String message) { + return new UserError(1, shortError, message); + } + + public static UserError UserEventLow(String shortError, String message) { + return new UserError(5, shortError, message); + } + + public static UserError UserEventHigh(String shortError, String message) { + return new UserError(6, shortError, message); + } + + // TODO move time calc stuff to JOH, wrap it here with our timestamp + public String bestTime() { + final long since = JoH.msSince(timestamp); + if (since < Constants.DAY_IN_MS) { + return JoH.hourMinuteString(timestamp); + } else { + return JoH.dateTimeText(timestamp); + } + } + + + public static void cleanup() { + new Cleanup().execute(deletable()); + } + + // used in unit testing + public static void cleanup(long timestamp) { + List userErrors = new Select() + .from(UserError.class) + .where("timestamp < ?", timestamp) + .orderBy("timestamp desc") + .execute(); + if (userErrors != null) Log.d(TAG, "cleanup UserError size=" + userErrors.size()); + new Cleanup().execute(userErrors); + } + + public static void cleanupByTimeAndClause(final long timestamp, final String clause) { + new Delete().from(UserError.class) + .where("timestamp < ?", timestamp) + .where(clause) + .execute(); + } + + public synchronized static void cleanupRaw() { + final long timestamp = JoH.tsl(); + cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS, "severity < 3"); + cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS * 3, "severity = 3"); + cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS * 7, "severity > 3"); + Cache.clear(); + } + + + public static List all() { + return new Select() + .from(UserError.class) + .orderBy("timestamp desc") + .execute(); + } + + public static List deletable() { + List userErrors = new Select() + .from(UserError.class) + .where("severity < ?", 3) + .where("timestamp < ?", (new Date().getTime() - 1000 * 60 * 60 * 24)) + .orderBy("timestamp desc") + .execute(); + List highErrors = new Select() + .from(UserError.class) + .where("severity = ?", 3) + .where("timestamp < ?", (new Date().getTime() - 1000*60*60*24*3)) + .orderBy("timestamp desc") + .execute(); + List events = new Select() + .from(UserError.class) + .where("severity > ?", 3) + .where("timestamp < ?", (new Date().getTime() - 1000*60*60*24*7)) + .orderBy("timestamp desc") + .execute(); + userErrors.addAll(highErrors); + userErrors.addAll(events); + return userErrors; + } + + public static List bySeverity(Integer[] levels) { + String levelsString = " "; + for (int level : levels) { + levelsString += level + ","; + } + Log.d("UserError", "severity in ("+levelsString.substring(0,levelsString.length() - 1)+")"); + return new Select() + .from(UserError.class) + .where("severity in ("+levelsString.substring(0,levelsString.length() - 1)+")") + .orderBy("timestamp desc") + .limit(10000)//too many data can kill akp + .execute(); + } + + public static List bySeverityNewerThanID(long id, Integer[] levels, int limit) { + String levelsString = " "; + for (int level : levels) { + levelsString += level + ","; + } + Log.d("UserError", "severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")"); + return new Select() + .from(UserError.class) + .where("_ID > ?", id) + .where("severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")") + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + public static List newerThanID(long id, int limit) { + return new Select() + .from(UserError.class) + .where("_ID > ?", id) + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + public static List olderThanID(long id, int limit) { + return new Select() + .from(UserError.class) + .where("_ID < ?", id) + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + public static List bySeverityOlderThanID(long id, Integer[] levels, int limit) { + String levelsString = " "; + for (int level : levels) { + levelsString += level + ","; + } + Log.d("UserError", "severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")"); + return new Select() + .from(UserError.class) + .where("_ID < ?", id) + .where("severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")") + .orderBy("timestamp desc") + .limit(limit) + .execute(); + } + + + public static UserError getForTimestamp(UserError error) { + try { + return new Select() + .from(UserError.class) + .where("timestamp = ?", error.timestamp) + .where("shortError = ?", error.shortError) + .where("message = ?", error.message) + .executeSingle(); + } catch (Exception e) { + Log.e(TAG,"getForTimestamp() Got exception on Select : "+e.toString()); + return null; + } + } + + private static class Cleanup extends AsyncTask, Integer, Boolean> { + @Override + protected Boolean doInBackground(List... errors) { + try { + for(UserError userError : errors[0]) { + userError.delete(); + //userError.save(); + } + return true; + } catch(Exception e) { + return false; + } + } + } + + public static List bySeverity(int level) { + return bySeverity(new Integer[]{level}); + } + public static List bySeverity(int level, int level2) { + return bySeverity(new Integer[]{ level, level2 }); + } + public static List bySeverity(int level, int level2, int level3) { + return bySeverity(new Integer[]{ level, level2, level3 }); + } + + + public static class Log { + public static void e(String a, String b){ + android.util.Log.e(a, b); + new UserError(a, b); + } + + public static void e(String tag, String b, Exception e){ + android.util.Log.e(tag, b, e); + new UserError(tag, b + "\n" + e.toString()); + } + + public static void w(String tag, String b){ + android.util.Log.w(tag, b); + UserError.UserErrorLow(tag, b); + } + public static void w(String tag, String b, Exception e){ + android.util.Log.w(tag, b, e); + UserError.UserErrorLow(tag, b + "\n" + e.toString()); + } + public static void wtf(String tag, String b){ + android.util.Log.wtf(tag, b); + UserError.UserErrorHigh(tag, b); + } + public static void wtf(String tag, String b, Exception e){ + android.util.Log.wtf(tag, b, e); + UserError.UserErrorHigh(tag, b + "\n" + e.toString()); + } + public static void wtf(String tag, Exception e){ + android.util.Log.wtf(tag, e); + UserError.UserErrorHigh(tag, e.toString()); + } + + public static void uel(String tag, String b) { + android.util.Log.i(tag, b); + UserError.UserEventLow(tag, b); + } + + public static void ueh(String tag, String b) { + android.util.Log.i(tag, b); + UserError.UserEventHigh(tag, b); + } + + public static void d(String tag, String b){ + android.util.Log.d(tag, b); + if(ExtraLogTags.shouldLogTag(tag, android.util.Log.DEBUG)) { + UserErrorLow(tag, b); + } + } + + public static void v(String tag, String b){ + android.util.Log.v(tag, b); + if(ExtraLogTags.shouldLogTag(tag, android.util.Log.VERBOSE)) { + UserErrorLow(tag, b); + } + } + + public static void i(String tag, String b){ + android.util.Log.i(tag, b); + if(ExtraLogTags.shouldLogTag(tag, android.util.Log.INFO)) { + UserErrorLow(tag, b); + } + } + + static ExtraLogTags extraLogTags = new ExtraLogTags(); + } + + public static class ExtraLogTags { + + static Hashtable extraTags; + ExtraLogTags () { + extraTags = new Hashtable (); + String extraLogs = Pref.getStringDefaultBlank("extra_tags_for_logging"); + readPreference(extraLogs); + } + + /* + * This function reads a string representing tags that the user wants to log + * Format of string is tag1:level1,tag2,level2 + * Example of string is Alerts:i,BG:W + * + */ + public static void readPreference(String extraLogs) { + extraLogs = extraLogs.trim(); + if (extraLogs.length() > 0) UserErrorLow(TAG, "called with string " + extraLogs); + extraTags.clear(); + + // allow splitting to work with a single entry and no delimiter zzz + if ((extraLogs.length() > 1) && (!extraLogs.contains(","))) { + extraLogs += ","; + } + String[] tags = extraLogs.split(","); + if (tags.length == 0) { + return; + } + + // go over all tags and parse them + for(String tag : tags) { + if (tag.length() > 0) parseTag(tag); + } + } + + static void parseTag(String tag) { + // Format is tag:level for example Alerts:i + String[] tagAndLevel = tag.trim().split(":"); + if(tagAndLevel.length != 2) { + Log.e(TAG, "Failed to parse " + tag); + return; + } + String level = tagAndLevel[1]; + String tagName = tagAndLevel[0].toLowerCase(); + if (level.compareTo("d") == 0) { + extraTags.put(tagName, android.util.Log.DEBUG); + UserErrorLow(TAG, "Adding tag with DEBUG " + tagAndLevel[0] ); + return; + } + if (level.compareTo("v") == 0) { + extraTags.put(tagName, android.util.Log.VERBOSE); + UserErrorLow(TAG,"Adding tag with VERBOSE " + tagAndLevel[0] ); + return; + } + if (level.compareTo("i") == 0) { + extraTags.put(tagName, android.util.Log.INFO); + UserErrorLow(TAG, "Adding tag with info " + tagAndLevel[0] ); + return; + } + Log.e(TAG, "Unknown level for tag " + tag + " please use d v or i"); + } + + static boolean shouldLogTag(final String tag, final int level) { + final Integer levelForTag = extraTags.get(tag != null ? tag.toLowerCase() : ""); + return levelForTag != null && level >= levelForTag; + } + + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserNotification.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserNotification.java new file mode 100644 index 0000000..56ddcda --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/UserNotification.java @@ -0,0 +1,182 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Created by Emma Black on 11/29/14. + */ + +@Table(name = "Notifications", id = BaseColumns._ID) +public class UserNotification extends Model { + + // For 'other alerts' this will be the time that the alert should be raised again. + // For calibration alerts this is the time that the alert was played. + @Column(name = "timestamp", index = true) + public double timestamp; + + @Column(name = "message") + public String message; + + @Column(name = "bg_alert") + public boolean bg_alert; + + @Column(name = "calibration_alert") + public boolean calibration_alert; + + @Column(name = "double_calibration_alert") + public boolean double_calibration_alert; + + @Column(name = "extra_calibration_alert") + public boolean extra_calibration_alert; + + @Column(name = "bg_unclear_readings_alert") + public boolean bg_unclear_readings_alert; + + @Column(name = "bg_missed_alerts") + public boolean bg_missed_alerts; + + @Column(name = "bg_rise_alert") + public boolean bg_rise_alert; + + @Column(name = "bg_fall_alert") + public boolean bg_fall_alert; + + private final static List legacy_types = Arrays.asList( + "bg_alert", "calibration_alert", "double_calibration_alert", + "extra_calibration_alert", "bg_unclear_readings_alert", + "bg_missed_alerts", "bg_rise_alert", "bg_fall_alert"); + private final static String TAG = AlertPlayer.class.getSimpleName(); + + + public static UserNotification lastBgAlert() { + return new Select() + .from(UserNotification.class) + .where("bg_alert = ?", true) + .orderBy("_ID desc") + .executeSingle(); + } + public static UserNotification lastCalibrationAlert() { + return new Select() + .from(UserNotification.class) + .where("calibration_alert = ?", true) + .orderBy("_ID desc") + .executeSingle(); + } + public static UserNotification lastDoubleCalibrationAlert() { + return new Select() + .from(UserNotification.class) + .where("double_calibration_alert = ?", true) + .orderBy("_ID desc") + .executeSingle(); + } + public static UserNotification lastExtraCalibrationAlert() { + return new Select() + .from(UserNotification.class) + .where("extra_calibration_alert = ?", true) + .orderBy("_ID desc") + .executeSingle(); + } + + // the UserNotifcation model is difficult to extend without adding more + // booleans which will introduce a database incompatibility and prevent + // downgrading. So instead we work around it with shared preferences until + // such time as the booleans are just replaced with a string field or similar + // improvement. + + public static UserNotification GetNotificationByType(String type) { + if (legacy_types.contains(type)) { + type = type + " = ?"; + return new Select() + .from(UserNotification.class) + .where(type, true) + .orderBy("_ID desc") + .executeSingle(); + } else { + final String timestamp = PersistentStore.getString("UserNotification:timestamp:" + type); + if (timestamp.equals("")) return null; + final String message = PersistentStore.getString("UserNotification:message:" + type); + if (message.equals("")) return null; + UserNotification userNotification = new UserNotification(); + userNotification.timestamp = JoH.tolerantParseDouble(timestamp, -1); + if (userNotification.timestamp == -1) return null; // bad data + userNotification.message = message; + Log.d(TAG, "Workaround for: " + type + " " + userNotification.message + " timestamp: " + userNotification.timestamp); + return userNotification; + } + } + + public static void DeleteNotificationByType(String type) { + if (legacy_types.contains(type)) { + UserNotification userNotification = UserNotification.GetNotificationByType(type); + if (userNotification != null) { + userNotification.delete(); + } + } else { + PersistentStore.setString("UserNotification:timestamp:" + type, ""); + } + } + + public static void snoozeAlert(String type, long snoozeMinutes) { + UserNotification userNotification = GetNotificationByType(type); + if(userNotification == null) { + Log.e(TAG, "Error snoozeAlert did not find an alert for type " + type); + return; + } + userNotification.timestamp = new Date().getTime() + snoozeMinutes * 60000; + userNotification.save(); + + } + + public static UserNotification create(String message, String type, long timestamp) { + UserNotification userNotification = new UserNotification(); + userNotification.timestamp = timestamp; + userNotification.message = message; + switch (type) { + case "bg_alert": + userNotification.bg_alert = true; + break; + case "calibration_alert": + userNotification.calibration_alert = true; + break; + case "double_calibration_alert": + userNotification.double_calibration_alert = true; + break; + case "extra_calibration_alert": + userNotification.extra_calibration_alert = true; + break; + case "bg_unclear_readings_alert": + userNotification.bg_unclear_readings_alert = true; + break; + case "bg_missed_alerts": + userNotification.bg_missed_alerts = true; + break; + case "bg_rise_alert": + userNotification.bg_rise_alert = true; + break; + case "bg_fall_alert": + userNotification.bg_fall_alert = true; + break; + default: + Log.d(TAG, "Saving workaround for: " + type + " " + message); + PersistentStore.setString("UserNotification:timestamp:" + type, String.format(Locale.US, "%d", (long) timestamp)); + PersistentStore.setString("UserNotification:message:" + type, message); + return null; + } + userNotification.save(); + return userNotification; + + } +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/Models/blueReader.java b/lib/nightscout/com/eveningoutpost/dexdrip/Models/blueReader.java new file mode 100644 index 0000000..d82c078 --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/Models/blueReader.java @@ -0,0 +1,220 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.text.format.DateFormat; +import com.eveningoutpost.dexdrip.Home; +import java.nio.ByteBuffer; +import java.util.Date; +import com.eveningoutpost.dexdrip.Services.DexCollectionService; +import com.eveningoutpost.dexdrip.Models.UserError.Log; +import com.eveningoutpost.dexdrip.UtilityModels.Notifications; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.xdrip; +import com.eveningoutpost.dexdrip.R; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.regex.*; + +import static com.eveningoutpost.dexdrip.utils.FileUtils.getExternalDir; +import static com.eveningoutpost.dexdrip.utils.FileUtils.makeSureDirectoryExists; + + +/** + * Created by MasterPlexus on 11.12.2017. + */ + +public class blueReader { + private static final String TAG = "blueReader"; + private static final String BatLog="/BatteryLog.csv"; + private static int counterHibernated = 0; + private static Matcher tempVers; + + private static final byte[] shutdown = new byte[]{0x6B}; // Char 'k' + private static final byte[] requestValue = new byte[]{0x6C}; // Char 'l' + private static final byte[] goHybernate = new byte[]{0x68}; // Char 'h' + private static final byte[] restart = new byte[]{0x79}; // Char 'y' + + + public static boolean isblueReader() { + final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first(); + try { + return activeBluetoothDevice.name.contentEquals("blueReader"); + } catch (NullPointerException e) { + return false; + } + } + + /* no real need at the moment 30.01.2018 + public static boolean isblueReaderPacket(byte[] buffer) { + return !((buffer == null) || (new String(buffer).startsWith("IDR") || + new String(buffer).startsWith("TRANS_FAILED") || + new String(buffer).startsWith("HYBERNATE SUCCESS") || + new String(buffer).startsWith("not ready for") || + new String(buffer).startsWith("NFC_DISABLED") ) + ); + } + */ + + public static byte[] decodeblueReaderPacket(byte[] buffer, int len) { + int cmdFound = 0; + long timestamp = new Date().getTime(); + String bufferstring; + //Log.w(TAG, "Packet: " + bufferstring); + if (buffer == null) { + Log.e(TAG, "null buffer passed to decodeblueReaderPacket"); + return null; + } else { + bufferstring=new String(buffer); + } + if (bufferstring.startsWith("not ready for") ) { //delete the trans_failed, because its normal only if the bluereader could not read the sensor. + counterHibernated++; + Log.e(TAG, "Found blueReader in a ugly State (" + counterHibernated + "/3), send hibernate to reset! If this does not help in the next 5 Minutes, then turn the bluereader manually off and on!"); + if (counterHibernated > 2) { + Log.wtf(TAG, "Ugly state not resolveable. Bluereader will be shut down! Please restart it!"); + Home.toaststatic("BlueReader ugly state not resolveable, bluereader will be shut down. Please restart it!"); + if (!Pref.getBooleanDefaultFalse("blueReader_suppressuglystatemsg")) { + Notifications.RiseDropAlert(xdrip.getAppContext(),true,"BlueReader Alarm", xdrip.getAppContext().getString(R.string.bluereaderuglystate),1); + } + return shutdown; + } else { + Home.toaststatic("Found blueReader in a ugly State, send hibernate to reset!"); + return goHybernate; //send hard hibernate, because blueReader is in a ugly state + } + } else if (bufferstring.startsWith("IDR")){ + Log.i(TAG, bufferstring); + PersistentStore.setString("blueReaderFirmware", bufferstring ); + tempVers=Pattern.compile(".*\\|blue(.*)-.*").matcher(bufferstring); + tempVers.find(); + PersistentStore.setDouble("blueReaderFirmwareValue",Double.parseDouble(tempVers.group(1))); + Log.i(TAG, "bluereader-Firmware-Version: " + tempVers); + if (BgReading.last() == null || BgReading.last().timestamp + (4 * 60 * 1000) < System.currentTimeMillis()) { + return requestValue; + } else { + return null; + } + } else if (bufferstring.startsWith("WAKE")) { + Log.d (TAG, "blueReader was set to wakeup-mode manually..."); + return null; + } else if (bufferstring.startsWith("ECHO")) { + Log.d (TAG, "blueReader was set to Echo-Mode manually..."); + return null; + } else if (bufferstring.startsWith("NFC READY")) { + Log.d (TAG, "blueReader notice that NFC is active..."); + return null; + } else if (bufferstring.startsWith("NFC_DISABLED")) { + Log.d (TAG, "blueReader notice that NFC is now hibernated..."); + return null; + } else if (bufferstring.startsWith("HYBERNATE SUCCESS")) { + Log.i (TAG, "blueReader notice that NFC is now really hibernated..."); + if (counterHibernated > 0) { + Log.w (TAG,"Found hibernation after wrong read. Resend read-command..."); + return requestValue; + } else { + return null; + } + } else if (bufferstring.startsWith("-r 0:")) { + Log.d (TAG, "blueReader sends an unknown reaction: '" + bufferstring + "'"); + return null; + } else if (bufferstring.startsWith("TRANS_FAILED")) { + Log.w (TAG, "Attention: check position of blueReader on the sensor, as it was not able to read!"); + Home.toaststatic(xdrip.getAppContext().getString(R.string.bluereader_position)); + return null; + } else if (bufferstring.startsWith("battery: ")) { + if (BgReading.last() == null || BgReading.last().timestamp + (4 * 60 * 1000) < System.currentTimeMillis()) { + return requestValue; + } + } else { + counterHibernated = 0; + processNewTransmitterData(TransmitterData.create(buffer, len, timestamp), timestamp); + // check for shutdown blueReader if Battery is too low + if (Pref.getBooleanDefaultFalse("blueReader_turn_off")) { + if (Pref.getInt("blueReader_turn_off_value",5) > Pref.getInt("bridge_battery",100)) { + Log.w (TAG, "blueReader will be turn off, as the battery is lower then " + Pref.getInt("blueReader_turn_off_value",5) +"%"); + Home.toaststatic(xdrip.getAppContext().getString(R.string.bluereaderoff) + Pref.getInt("blueReader_turn_off_value",5) +"%"); + return shutdown; + } + } + } + + return null; + } + + private static synchronized void processNewTransmitterData(TransmitterData transmitterData, long timestamp) { + if (transmitterData == null) { + return; + } + + final Sensor sensor = Sensor.currentSensor(); + if (sensor == null) { + Log.i(TAG, "setSerialDataToTransmitterRawData: No Active Sensor, Data only stored in Transmitter Data"); + return; + } + + if(PersistentStore.getLong("blueReader_Full_Battery") <3000 ) + PersistentStore.setLong("blueReader_Full_Battery", 4100); + + double blueReaderDays =0; + if (transmitterData.sensor_battery_level > PersistentStore.getLong("blueReader_Full_Battery")) { + PersistentStore.setLong("blueReader_Full_Battery", transmitterData.sensor_battery_level); + Log.i(TAG, "blueReader_Full_Battery set to: " + transmitterData.sensor_battery_level) ; + } + int localBridgeBattery =((transmitterData.sensor_battery_level - 3300) * 100 / (((int) (long) PersistentStore.getLong("blueReader_Full_Battery"))-3300)); + Pref.setInt("bridge_battery", localBridgeBattery); + sensor.latest_battery_level = localBridgeBattery; + blueReaderDays = 6.129200670865791d / (1d + Math.pow(((double)transmitterData.sensor_battery_level/3763.700630306379d),(-61.04241888028577d))); //todo compare with test-formular, and new Data of batterylog + if (transmitterData.sensor_battery_level < 3600) { + blueReaderDays=blueReaderDays + 0.1d; + } + blueReaderDays = ((Math.round((blueReaderDays)*10d)/10d)); + + PersistentStore.setString("bridge_battery_days", String.valueOf(blueReaderDays)); + sensor.save(); + if (Pref.getBooleanDefaultFalse("blueReader_writebatterylog")) { + final String dir = getExternalDir(); + makeSureDirectoryExists(dir); + writeLog(dir + BatLog, + DateFormat.format("yyyyMMdd-kkmmss", System.currentTimeMillis()).toString() + "|" + + PersistentStore.getLong("blueReader_Full_Battery") + "|" + + transmitterData.sensor_battery_level + "|" + + sensor.latest_battery_level + "|" + + blueReaderDays + ); + } + DexCollectionService.last_transmitter_Data = transmitterData; + Log.d(TAG, "BgReading.create: new BG reading at " + timestamp + " with a timestamp of " + transmitterData.timestamp); + BgReading.create(transmitterData.raw_data, transmitterData.filtered_data, xdrip.getAppContext(), transmitterData.timestamp); + + } + + + public static ByteBuffer initialize() { + Log.i(TAG, "initialize blueReader!"); + Pref.setInt("bridge_battery", 0); + PersistentStore.setDouble("blueReaderFirmwareValue", 0); + + //command to get Firmware + ByteBuffer ackMessage = ByteBuffer.allocate(3); + ackMessage.put(0, (byte) 0x49); + ackMessage.put(1, (byte) 0x44); + ackMessage.put(2, (byte) 0x4E); + return ackMessage; + } + + private static void writeLog(String logFile, String logLine) { + PrintWriter pWriter = null; + try { + pWriter = new PrintWriter(new BufferedWriter(new FileWriter(logFile, true))); + pWriter.println(logLine); + } catch (IOException ioe) { + Log.w(TAG, "log write error: " + ioe.toString()); + } finally { + if (pWriter != null){ + pWriter.flush(); + pWriter.close(); + } + } + } + +} diff --git a/lib/nightscout/com/eveningoutpost/dexdrip/utils/CipherUtils.java b/lib/nightscout/com/eveningoutpost/dexdrip/utils/CipherUtils.java new file mode 100644 index 0000000..d98860e --- /dev/null +++ b/lib/nightscout/com/eveningoutpost/dexdrip/utils/CipherUtils.java @@ -0,0 +1,220 @@ +package com.eveningoutpost.dexdrip.utils; + +/** + * Created by jamorham on 06/01/16. + */ + +import android.util.Base64; +import android.util.Log; + +//import com.eveningoutpost.dexdrip.GoogleDriveInterface; +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class CipherUtils { + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + static final String TAG = "jamorham cip"; + static final byte[] errorbyte = {}; + static String key = "ebe5c0df162a50ba232d2d721ea8e3e1c5423bb0-12bd-48c3-8932-c93883dfcf1f"; // allow override from prefs + + public static byte[] encrypt(byte[] ivBytes, byte[] keyBytes, byte[] textBytes) { + try { + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec); + return cipher.doFinal(textBytes); + } catch (Exception e) { + Log.e(TAG, "Error during encryption: " + e.toString()); + return errorbyte; + } + } + + public static byte[] decrypt(byte[] ivBytes, byte[] keyBytes, byte[] textBytes) { + try { + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec); + return cipher.doFinal(textBytes); + } catch (Exception e) { + return errorbyte; + } + } + + private static byte[] getKeyBytes(String mykey) { + try { + MessageDigest digest = java.security.MessageDigest + .getInstance("MD5"); + digest.update(mykey.getBytes(Charset.forName("UTF-8"))); + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Password creation exception: " + e.toString()); + return errorbyte; + } + } + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexToBytes(String hex) { + try { + int length = hex.length(); + byte[] bytes = new byte[length / 2]; + for (int i = 0; i < length; i += 2) { + bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); + } + return bytes; + } catch (Exception e){ + Log.e(TAG,"Got Exception: "+e.toString()); + return new byte[0]; + } + } + + public static String getSHA256(byte[] mydata) { + try { + MessageDigest digest = java.security.MessageDigest + .getInstance("SHA256"); + digest.update(mydata); + return bytesToHex(digest.digest()).toLowerCase(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "SHA hash exception: " + e.toString()); + return null; + } + } + + public static String getSHA256(String mykey) { + try { + MessageDigest digest = java.security.MessageDigest + .getInstance("SHA256"); + digest.update(mykey.getBytes(Charset.forName("UTF-8"))); + return bytesToHex(digest.digest()).toLowerCase(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "SHA hash exception: " + e.toString()); + return null; + } + } + + public static String getMD5(String mykey) { + try { + MessageDigest digest = java.security.MessageDigest + .getInstance("MD5"); + digest.update(mykey.getBytes(Charset.forName("UTF-8"))); + return bytesToHex(digest.digest()).toLowerCase(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "MD5 hash exception: " + e.toString()); + return null; + } + } + + public static byte[] encryptBytes(byte[] plainText) { + byte[] keyBytes = getKeyBytes(key); + return encryptBytes(plainText, keyBytes); + } + + public static byte[] encryptBytes(byte[] plainText, byte[] keyBytes) { + byte[] ivBytes = new byte[16]; + + if ((keyBytes == null) || (keyBytes.length != 16)) { + Log.e(TAG, "Invalid Keybytes length!"); + return errorbyte; + } + SecureRandom sr = new SecureRandom(); + sr.nextBytes(ivBytes); + byte[] cipherData = CipherUtils.encrypt(ivBytes, keyBytes, plainText); + byte[] destination = new byte[cipherData.length + ivBytes.length]; + System.arraycopy(ivBytes, 0, destination, 0, ivBytes.length); + System.arraycopy(cipherData, 0, destination, ivBytes.length, cipherData.length); + return destination; + } + public static byte[] decryptBytes(byte[] cipherData) { + byte[] keyBytes = getKeyBytes(key ); + return decryptBytes(cipherData,keyBytes); + } + public static byte[] decryptBytes(byte[] cipherData,byte[] keyBytes) { + byte[] ivBytes = new byte[16]; + if (cipherData.length < ivBytes.length) return errorbyte; + if ((keyBytes==null) || (keyBytes.length != 16)) { + Log.e(TAG, "Invalid Keybytes length!"); + return errorbyte; + } + System.arraycopy(cipherData, 0, ivBytes, 0, ivBytes.length); + byte[] destination = new byte[cipherData.length - ivBytes.length]; + System.arraycopy(cipherData, ivBytes.length, destination, 0, destination.length); + return CipherUtils.decrypt(ivBytes, keyBytes, destination); + } + + public static String decryptString(String cipherData) { + try { + return new String(decryptStringToBytes(cipherData), "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Got unsupported encoding on UTF8 " + e.toString()); + return ""; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Got IllegalArgumentException encoding on UTF8 ", e); + return ""; + } + } + + public static byte[] decryptStringToBytes(String cipherData) { + byte[] inbytes = Base64.decode(cipherData, Base64.NO_WRAP); + byte[] decryptedBytes = decryptBytes(inbytes); + if ((decryptedBytes.length > 8) + && (decryptedBytes[0] == (byte) 0x1F) + && (decryptedBytes[1] == (byte) 0x8B) + && (decryptedBytes[2] == (byte) 0x08) + && (decryptedBytes[3] == (byte) 0x00)) { + decryptedBytes = JoH.decompressBytesToBytes(decryptedBytes); + } + return decryptedBytes; + } + + public static String encryptString(String plainText) { + byte[] inbytes = plainText.getBytes(Charset.forName("UTF-8")); + return Base64.encodeToString(encryptBytes(inbytes), Base64.NO_WRAP); + } + + public static String encryptBytesToString(byte[] inbytes) { + return Base64.encodeToString(encryptBytes(inbytes), Base64.NO_WRAP); + } + + public static String compressEncryptString(String plainText) { + return Base64.encodeToString(CipherUtils.encryptBytes(JoH.compressStringToBytes(plainText)) + , Base64.NO_WRAP); + } + + public static String compressEncryptBytes(byte[] plainText) { + return Base64.encodeToString(CipherUtils.encryptBytes(JoH.compressBytesToBytes(plainText)) + , Base64.NO_WRAP); + } + + public static byte[] getRandomKey() { + byte[] keybytes = new byte[16]; + SecureRandom sr = new SecureRandom(); + sr.nextBytes(keybytes); + return keybytes; + } + + public static String getRandomHexKey() { + return bytesToHex(getRandomKey()); + } +} + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7d6bf6e --- /dev/null +++ b/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + 1.11 + 1.11 + + + + com.chrispr + DexcomBluetoothUploader + 1.0-SNAPSHOT + + + org.projectlombok + lombok + 1.18.12 + provided + + + + \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000..b7d3043 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.chrispr.Main + diff --git a/src/main/java/com/chrispr/Main.java b/src/main/java/com/chrispr/Main.java new file mode 100644 index 0000000..ce9f404 --- /dev/null +++ b/src/main/java/com/chrispr/Main.java @@ -0,0 +1,56 @@ +package com.chrispr; +import com.chrispr.bluetooth.BluetoothScanner; +import com.github.hypfvieh.bluetooth.*; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService; +import io.github.s5uishida.iot.bluetooth.scanner.IScanHandler; + +import io.github.s5uishida.iot.bluetooth.scanner.ScanData; +import io.github.s5uishida.iot.bluetooth.scanner.ScanProcess; +import org.apache.log4j.BasicConfigurator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Main { + + public static void main(String[] args) { + // write your code here + + //BluetoothScanner scanner = new BluetoothScanner(); + //scanner.StartScan(); + BasicConfigurator.configure(); + Map filter = new HashMap(); + filter.put(DiscoveryFilter.Transport, DiscoveryTransport.LE); + + ScanProcess scanProcess = new ScanProcess("hci0", new MyScanHandler(), filter); + //ScanProcess scanProcess = new ScanProcess("hci0", new MyScanHandler()); + scanProcess.start(); + + } +} + +class MyScanHandler implements IScanHandler { + private static final Logger LOG = LoggerFactory.getLogger(MyScanHandler.class); + + @Override + public void handle(BluetoothDevice device, ScanData data) { + LOG.info(device.toString()); + LOG.info(data.toString()); + + String deviceName = device.getName(); + if(deviceName.startsWith("Dexcom")) { + //Found it + device.connect(); + + List services = device.getGattServices(); + for(BluetoothGattService service :services) { + LOG.info(service.toString()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chrispr/bluetooth/BluetoothDexcomService.java b/src/main/java/com/chrispr/bluetooth/BluetoothDexcomService.java new file mode 100644 index 0000000..1eba440 --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/BluetoothDexcomService.java @@ -0,0 +1,15 @@ +package com.chrispr.bluetooth; + +import com.github.hypfvieh.bluetooth.DeviceManager; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BluetoothDexcomService { + private static final Logger log = LoggerFactory.getLogger(BluetoothDexcomService.class); + + + +} diff --git a/src/main/java/com/chrispr/bluetooth/BluetoothScanner.java b/src/main/java/com/chrispr/bluetooth/BluetoothScanner.java new file mode 100644 index 0000000..82ad675 --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/BluetoothScanner.java @@ -0,0 +1,42 @@ +package com.chrispr.bluetooth; +import com.github.hypfvieh.bluetooth.DeviceManager; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice; +import org.bluez.Device1; +import org.freedesktop.dbus.exceptions.DBusException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Set; + +public class BluetoothScanner { + private static final Logger LOG = LoggerFactory.getLogger(BluetoothScanner.class); + private DeviceManager btManager; + private BluetoothAdapter btAdapter; + public BluetoothScanner() { + this(""); + } + public BluetoothScanner(String adapterName) { + + + try { + DeviceManager.createInstance(false); + } catch (DBusException e) { + e.printStackTrace(); + } + btManager = DeviceManager.getInstance(); + List adapters = btManager.getAdapters(); + for(BluetoothAdapter adapter : adapters) + { + if(adapterName.length() == 0 || adapter.getDeviceName().contains(adapterName)) { + LOG.info("Chose adapter {}", adapter.getDeviceName()); + btAdapter = adapter; + } + } + } + public void StartScan() { + List devices = btManager.getDevices(); + + } +} diff --git a/src/main/java/com/chrispr/bluetooth/DexcomConnectionState.java b/src/main/java/com/chrispr/bluetooth/DexcomConnectionState.java new file mode 100644 index 0000000..33b6997 --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/DexcomConnectionState.java @@ -0,0 +1,4 @@ +package com.chrispr.bluetooth; + +public class DexcomConnectionState { +}