Files
2020-07-18 21:44:27 -04:00

1687 lines
83 KiB
Java

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.
* <p>
* 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<Ob1Work> 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<BgReading> 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<Ob1Work> 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<ArrayList<Ob1Work>>() {
}.getType();
final List<Ob1Work> 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<BackFillStream.Backsie> 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";
}
}