package com.eveningoutpost.dexdrip.insulin.inpen; import android.annotation.TargetApi; import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.os.Build; import android.os.PowerManager; import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.PenData; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer; 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.insulin.inpen.messages.AdvertRx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.BatteryRx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.BondTx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.KeepAliveTx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.RecordRx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.RecordTx; import com.eveningoutpost.dexdrip.insulin.inpen.messages.TimeRx; import com.eveningoutpost.dexdrip.insulin.shared.ProcessPenData; import com.eveningoutpost.dexdrip.utils.bt.Helper; import com.eveningoutpost.dexdrip.utils.framework.WakeLockTrampoline; import com.eveningoutpost.dexdrip.utils.math.Converters; import com.eveningoutpost.dexdrip.utils.time.SlidingWindowConstraint; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.polidea.rxandroidble2.RxBleDeviceServices; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; import com.polidea.rxandroidble2.exceptions.BleGattCharacteristicException; import com.polidea.rxandroidble2.exceptions.BleGattException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; //import rx.schedulers.Schedulers; import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static android.bluetooth.BluetoothDevice.BOND_BONDED; import static android.bluetooth.BluetoothDevice.BOND_NONE; import static com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump.dumpHexString; import static com.eveningoutpost.dexdrip.Models.JoH.bytesToHex; import static com.eveningoutpost.dexdrip.Models.JoH.dateTimeText; import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; import static com.eveningoutpost.dexdrip.Models.JoH.hourMinuteString; import static com.eveningoutpost.dexdrip.Models.JoH.msSince; import static com.eveningoutpost.dexdrip.Models.JoH.quietratelimit; import static com.eveningoutpost.dexdrip.Models.JoH.ratelimit; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSE; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.INIT; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.INPEN_SERVICE_FAILOVER_ID; 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.UtilityModels.StatusItem.Highlight.BAD; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.GOOD; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NORMAL; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.AUTHENTICATION; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.BATTERY; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.BONDCONTROL; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.HEXDUMP_INFO_CHARACTERISTICS; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.INFO_CHARACTERISTICS; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.KEEPALIVE; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.PEN_ATTACH_TIME; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.PEN_TIME; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.PRINTABLE_INFO_CHARACTERISTICS; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.RECORD_END; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.RECORD_INDEX; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.RECORD_INDICATE; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.RECORD_REQUEST; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.RECORD_START; import static com.eveningoutpost.dexdrip.insulin.inpen.Constants.REMAINING_INDEX; import static com.eveningoutpost.dexdrip.insulin.inpen.InPen.DEFAULT_BOND_UNITS; import static com.eveningoutpost.dexdrip.insulin.inpen.InPen.STORE_INPEN_ADVERT; import static com.eveningoutpost.dexdrip.insulin.inpen.InPen.STORE_INPEN_BATTERY; import static com.eveningoutpost.dexdrip.insulin.inpen.InPen.STORE_INPEN_INFOS; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenEntry.ID_INPEN; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenEntry.isStarted; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.BONDAGE; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.BOND_AUTHORITY; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_AUTH_STATE; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_AUTH_STATE2; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_A_TIME; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_BATTERY; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_IDENTITY; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_INDEX; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_RECORDS; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.GET_TIME; import static com.eveningoutpost.dexdrip.insulin.inpen.InPenService.InPenState.KEEP_ALIVE; import static com.eveningoutpost.dexdrip.utils.bt.Helper.getCharactersticName; /** jamorham * * InPen connection and data transfer service */ public class InPenService extends JamBaseBluetoothSequencer { private static final boolean D = false; private static final int MAX_BACKLOG = 80; private final ConcurrentLinkedQueue<byte[]> records = new ConcurrentLinkedQueue<>(); private static TimeRx currentPenTime = null; // TODO hashmap for multiple pens? private static TimeRx currentPenAttachTime = null; // TODO hashmap for multiple pens? private int lastIndex = -1; // TODO hashmap for multiple pens? Use storage object for each pen?? private int gotIndex = -2; // TODO hashmap for multiple pens? Use storage object for each pen?? private static volatile String lastState = "None"; private static volatile String lastError = null; private static volatile long lastStateUpdated = -1; private static volatile long lastReceivedData = -1; private static volatile boolean needsAuthentication = false; private static int lastBattery = -1; private static PenData lastPenData = null; private static boolean infosLoaded = false; private static boolean gotAll = false; private static ConcurrentHashMap<UUID, Object> staticCharacteristics; private static PendingIntent serviceFailoverIntent; private static long failover_time; { mState = new InPenState().setLI(I); I.backgroundStepDelay = 0; I.autoConnect = true; I.autoReConnect = true; // TODO control these two from preference? I.playSounds = true; I.connectTimeoutMinutes = 25; I.reconnectConstraint = new SlidingWindowConstraint(30, MINUTE_IN_MS, "max_reconnections"); //I.resetWhenAlreadyConnected = true; } static class InPenState extends JamBaseBluetoothSequencer.BaseState { static final String PROTOTYPE = "Prototype Test"; static final String GET_IDENTITY = "Get Identity"; static final String KEEP_ALIVE = "Keep Alive"; static final String BOND_AUTHORITY = "Bond Authority"; static final String GET_AUTH_STATE = "Get Auth"; static final String GET_AUTH_STATE2 = "Get Post Auth"; static final String BONDAGE = "Bonding"; static final String GET_BATTERY = "Get Battery"; static final String GET_INDEX = "Get Index"; static final String GET_TIME = "Get Time"; static final String GET_A_TIME = "Get Attach Time"; static final String GET_RECORDS = "Get Records"; { sequence.clear(); sequence.add(INIT); sequence.add(CONNECT_NOW); sequence.add(DISCOVER); // automatically executed sequence.add(GET_IDENTITY); sequence.add(GET_A_TIME); sequence.add(GET_AUTH_STATE); sequence.add(KEEP_ALIVE); sequence.add(BOND_AUTHORITY); sequence.add(BONDAGE); // TODO wait for bonding to be bonded sequence.add(GET_AUTH_STATE2); sequence.add(SLEEP); // sequence.add(GET_INDEX); sequence.add(GET_TIME); sequence.add(GET_BATTERY); sequence.add(GET_RECORDS); // tends to close connection after this sequence.add(SLEEP); // sequence.add(PROTOTYPE); sequence.add(SEND_QUEUE); sequence.add(SLEEP); // } } @Override protected synchronized boolean automata() { extendWakeLock(1000); if (D) UserError.Log.d(TAG, "Automata called in InPen"); msg(I.state); switch (I.state) { case INIT: // connect by default changeNextState(); break; case GET_IDENTITY: getIdentity(null); break; case GET_BATTERY: getBattery(); break; case GET_A_TIME: getAttachTime(); break; case GET_TIME: getTime(); break; case GET_RECORDS: getRecords(); break; case KEEP_ALIVE: keepAlive(); break; case BOND_AUTHORITY: bondAuthority(); break; case BONDAGE: bondAsRequired(true); break; case GET_AUTH_STATE: case GET_AUTH_STATE2: getAuthState(); break; case GET_INDEX: getIndex(); break; default: if (shouldServiceRun()) { if (msSince(lastReceivedData) < MINUTE_IN_MS) { Inevitable.task("inpen-set-failover", 1000, this::setFailOverTimer); } return super.automata(); } else { UserError.Log.d(TAG, "Service should be shut down so stopping automata"); } } return true; // lies } @Override public int onStartCommand(Intent intent, int flags, int startId) { final PowerManager.WakeLock wl = JoH.getWakeLock("inpen service", 60000); try { InPenEntry.started_at = JoH.tsl(); UserError.Log.d(TAG, "WAKE UP WAKE UP WAKE UP"); if (shouldServiceRun()) { final String mac = InPen.getMac(); // load from settings class if (emptyString(mac)) { // if mac not set then start up a scan and do nothing else new FindNearby().scan(); } else { setAddress(mac); commonServiceStart(); if (intent != null) { final String function = intent.getStringExtra("function"); if (function != null) { switch (function) { case "failover": changeState(CLOSE); break; case "reset": JoH.static_toast_long("Searching for Pen"); InPen.setMac(""); InPenEntry.startWithRefresh(); break; case "refresh": currentPenAttachTime = null; currentPenTime = null; changeState(INIT); break; case "prototype": // changeState(PROTOTYPE); break; } } } } setFailOverTimer(); return START_STICKY; } else { UserError.Log.d(TAG, "Service is NOT set be active - shutting down"); stopSelf(); return START_NOT_STICKY; } } finally { JoH.releaseWakeLock(wl); } } private void commonServiceStart() { I.playSounds = false; } @Override public void onDestroy() { super.onDestroy(); InPenEntry.started_at = -1; } ///// Methods private void getAuthState() { I.connection.readCharacteristic(AUTHENTICATION).subscribe( readValue -> { UserError.Log.d(TAG, "Authentication result: " + dumpHexString(readValue)); authenticationProcessor(readValue); }, throwable -> { UserError.Log.e(TAG, "Could not read after Authentication status: " + throwable); changeState(CLOSE); }); } private void authenticationProcessor(final byte[] value) { if (value == null || value.length < 1 || value[0] != 0) { // "U" = unbonded? UserError.Log.d(TAG, "authenticationProcessor: not authenticated: " + bytesToHex(value)); needsAuthentication = true; changeNextState(); // not authenticated } else { UserError.Log.d(TAG, "authenticationProcessor: we are authenticated: " + bytesToHex(value)); // we are authenticated! needsAuthentication = false; changeState(GET_INDEX); } } // TODO make sure we don't get stuck on a bum record private boolean checkMissingIndex() { final long missingIndex = PenData.getMissingIndex(I.address); if (missingIndex != -1) { UserError.Log.d(TAG, "Index: " + missingIndex + " is missing"); getRecords((int) missingIndex, (int) missingIndex); return true; } return false; } private void getIndex() { I.connection.readCharacteristic(RECORD_INDEX).subscribe( indexValue -> { UserError.Log.d(TAG, "GetIndex result: " + dumpHexString(indexValue)); lastReceivedData = JoH.tsl(); I.connection.readCharacteristic(REMAINING_INDEX).subscribe( remainingValue -> indexProcessor(indexValue, remainingValue), throwable -> UserError.Log.e(TAG, "Could not read after Remaining status: " + throwable)); }, throwable -> UserError.Log.e(TAG, "Could not read after Index status: " + throwable)); } private void indexProcessor(final byte[] indexValue, final byte[] remainingValue) { lastIndex = Converters.unsignedBytesToInt(indexValue); gotAll = lastIndex == gotIndex; UserError.Log.d(TAG, "Index value: " + lastIndex); UserError.Log.d(TAG, "Remain value: " + Converters.unsignedBytesToInt(remainingValue)); changeNextState(); } private void getTime() { // TODO persist epoch if (currentPenTime == null || ratelimit("inpen-get-time", 10000)) { I.connection.readCharacteristic(PEN_TIME).subscribe( timeValue -> { UserError.Log.d(TAG, "GetTime result: " + dumpHexString(timeValue)); currentPenTime = new TimeRx().fromBytes(timeValue); if (currentPenTime != null) { UserError.Log.d(TAG, "Current pen epoch: " + JoH.dateTimeText(currentPenTime.getPenEpoch())); changeNextState(); } else { UserError.Log.e(TAG, "Current pen time invalid"); } }, throwable -> UserError.Log.e(TAG, "Could not read after get time status: " + throwable)); } else { UserError.Log.d(TAG, "Skipping get time, already have epoch"); changeNextState(); } } private void getAttachTime() { // TODO persist attach epoch if (currentPenAttachTime == null || ratelimit("inpen-get-time", 180000)) { I.connection.readCharacteristic(PEN_ATTACH_TIME).subscribe( timeValue -> { UserError.Log.d(TAG, "GetAttachTime result: " + dumpHexString(timeValue)); currentPenAttachTime = new TimeRx().fromBytes(timeValue); if (currentPenAttachTime != null) { UserError.Log.d(TAG, "Current pen attach epoch: " + currentPenAttachTime.getPenTime()); changeNextState(); } else { UserError.Log.e(TAG, "Current pen attach time invalid"); } }, throwable -> { UserError.Log.e(TAG, "Could not read after get attach time status: " + throwable); if (throwable instanceof BleDisconnectedException) { changeState(CLOSE); } else { changeNextState(); } }); } else { UserError.Log.d(TAG, "Skipping get attach time, already have epoch"); changeNextState(); } } private void getBattery() { if (JoH.pratelimit("inpen-battery-poll-" + I.address, 40000)) { I.connection.readCharacteristic(BATTERY).subscribe( batteryValue -> { final BatteryRx battery = new BatteryRx().fromBytes(batteryValue); if (battery != null) { lastBattery = battery.getBatteryPercent(); PersistentStore.setLong(STORE_INPEN_BATTERY + I.address, lastBattery); UserError.Log.d(TAG, "GetBattery result: " + battery.getBatteryPercent()); changeNextState(); } else { UserError.Log.e(TAG, "Invalid GetBattery result: " + dumpHexString(batteryValue)); changeNextState(); } }, throwable -> { UserError.Log.e(TAG, "Could not read after Battery status: " + throwable); changeNextState(); }); } else { UserError.Log.d(TAG, "Skipping battery read"); if (lastBattery == -1) { lastBattery = (int) PersistentStore.getLong(STORE_INPEN_BATTERY + I.address); if (lastBattery == 0) { lastBattery = -1; } else { UserError.Log.d(TAG, "Loaded battery from store: " + lastBattery); } } changeNextState(); } } private void getIdentity(final Queue<UUID> queue) { // CHECK IF WE ALREADY HAVE if (queue == null) { UserError.Log.d(TAG, "IDENTITY: creating queue: " + I.characteristics.size()); loadInfos(); final ConcurrentLinkedQueue<UUID> newQueue = new ConcurrentLinkedQueue<>(); for (final UUID uuid : INFO_CHARACTERISTICS) { if (I.characteristics.containsKey(uuid)) { if (!(I.characteristics.get(uuid) instanceof byte[])) { newQueue.add(uuid); } else { UserError.Log.d(TAG, "Already have value for: " + getCharactersticName(uuid.toString())); } } else { UserError.Log.d(TAG, "Characteristic not found in discover services: " + getCharactersticName(uuid.toString())); } } if (!newQueue.isEmpty()) { getIdentity(newQueue); } else { // already got everything changeNextState(); } return; } if (!queue.isEmpty()) { final UUID uuid = queue.poll(); UserError.Log.d(TAG, "IDENTITY: list not empty: uuid: " + uuid); I.connection.readCharacteristic(uuid).timeout(5, TimeUnit.SECONDS).subscribe( infoValue -> { UserError.Log.d(TAG, getCharactersticName(uuid.toString()) + " result: " + dumpHexString(infoValue)); I.characteristics.put(uuid, infoValue); staticCharacteristics = I.characteristics; getIdentity(queue); }, throwable -> { UserError.Log.e(TAG, "Could not read after " + getCharactersticName(uuid.toString()) + " status: " + throwable); changeNextState(); }); } else { UserError.Log.d(TAG, "Info Queue empty"); saveInfos(); changeNextState(); } } private void loadInfos() { try { if (infosLoaded) return; for (Map.Entry<UUID, Object> entrySet : I.characteristics.entrySet()) { if (entrySet.getValue() instanceof byte[]) { UserError.Log.d(TAG, "Found item skipping load infos"); return; } } final Gson gson = new GsonBuilder().create(); final String json = PersistentStore.getString(STORE_INPEN_INFOS + I.address); if (D) UserError.Log.d(TAG, "JSON: " + json); if (json.length() > 10) { final HashMap<UUID, Object> loaded = gson.fromJson(json, new TypeToken<Map<UUID, Object>>() { }.getType()); for (Map.Entry<UUID, Object> entry : loaded.entrySet()) { // this seems like an excessive amount of gymnastics to deserialize.. if (entry.getValue() instanceof ArrayList) { if (D) UserError.Log.d(TAG, "Populating for: " + (entry.getKey())); final ArrayList<Double> bytelist = ((ArrayList<Double>) entry.getValue()); final byte[] bytes = new byte[bytelist.size()]; for (int i = 0; i < bytes.length; i++) { bytes[i] = bytelist.get(i).byteValue(); } if (D) UserError.Log.d(TAG, entry.getKey() + " " + JoH.bytesToHex(bytes)); I.characteristics.put(entry.getKey(), bytes); } } } staticCharacteristics = I.characteristics; infosLoaded = true; UserError.Log.d(TAG, "loadInfos() loaded"); } catch (Exception e) { UserError.Log.wtf(TAG, "Got exception in loadInfos " + e); } } private void saveInfos() { final Gson gson = new GsonBuilder().create(); final String json = gson.toJson(staticCharacteristics); PersistentStore.setString(STORE_INPEN_INFOS + I.address, json); UserError.Log.d(TAG, json); } private void getRecords() { if (checkMissingIndex()) return; // processing from there instead if (lastIndex < 0) { UserError.Log.e(TAG, "Cannot get records as index is not defined"); return; } final long highest = PenData.getHighestIndex(I.address); int firstIndex = highest > 0 ? (int) highest + 1 : 1; if (firstIndex > lastIndex) { UserError.Log.e(TAG, "First index is greater than last index: " + firstIndex + " " + lastIndex); return; } final int count = lastIndex - firstIndex; if (count > MAX_BACKLOG) { firstIndex = lastIndex - MAX_BACKLOG; UserError.Log.d(TAG, "Restricting first index to: " + firstIndex); } getRecords(firstIndex, lastIndex); } private void getRecords(final int firstIndex, final int lastIndex) { final int numberOfRecords = lastIndex - firstIndex; if (numberOfRecords > 30) { I.connection.writeCharacteristic(KEEPALIVE, new KeepAliveTx().getBytes()).subscribe( value -> { UserError.Log.d(TAG, "Wrote keep alive for " + numberOfRecords); }, throwable -> { UserError.Log.d(TAG, "Got exception in keep alive" + throwable); }); } final RecordTx packet = new RecordTx(firstIndex, lastIndex); UserError.Log.d(TAG, "getRecords called, loading: " + firstIndex + " to " + lastIndex); I.connection.setupIndication(RECORD_INDICATE).doOnNext(notificationObservable -> { I.connection.writeCharacteristic(RECORD_START, packet.startBytes()).subscribe(valueS -> { UserError.Log.d(TAG, "Wrote record start: " + bytesToHex(valueS)); I.connection.writeCharacteristic(RECORD_END, packet.endBytes()).subscribe(valueE -> { UserError.Log.d(TAG, "Wrote record end: " + bytesToHex(valueE)); I.connection.writeCharacteristic(RECORD_REQUEST, packet.triggerBytes()).subscribe( characteristicValue -> { if (D) UserError.Log.d(TAG, "Wrote record request request: " + bytesToHex(characteristicValue)); }, throwable -> { UserError.Log.e(TAG, "Failed to write record request: " + throwable); if (throwable instanceof BleGattCharacteristicException) { final int status = ((BleGattCharacteristicException) throwable).getStatus(); UserError.Log.e(TAG, "Got status message: " + Helper.getStatusName(status)); } }); }, throwable -> { UserError.Log.d(TAG, "Throwable in Record End write: " + throwable); }); }, throwable -> { UserError.Log.d(TAG, "Throwable in Record Start write: " + throwable); // throws BleGattCharacteristicException status = 128 for "no resources" eg nothing matches }); }) .flatMap(notificationObservable -> notificationObservable) .timeout(120, TimeUnit.SECONDS) .observeOn(Schedulers.newThread()) .subscribe(bytes -> { records.add(bytes); UserError.Log.d(TAG, "INDICATE INDICATE: " + HexDump.dumpHexString(bytes)); }, throwable -> { if (!(throwable instanceof OperationSuccess)) { if (throwable instanceof BleDisconnectedException) { UserError.Log.d(TAG, "Disconnected when waiting to receive indication: " + throwable); } else { UserError.Log.e(TAG, "Error receiving indication: " + throwable); } Inevitable.task("check-records-queue", 100, this::processRecordsQueue); } }); } private synchronized void processRecordsQueue() { boolean newRecord = false; while (!records.isEmpty()) { final byte[] record = records.poll(); if (record != null) { final RecordRx recordRx = new RecordRx(currentPenTime).fromBytes(record); if (recordRx != null) { UserError.Log.d(TAG, "RECORD RECORD: " + recordRx.toS()); final PenData penData = PenData.create(I.address, ID_INPEN, recordRx.index, recordRx.units, recordRx.getRealTimeStamp(), recordRx.temperature, record); if (penData == null) { UserError.Log.wtf(TAG, "Error creating PenData record from " + HexDump.dumpHexString(record)); } else { penData.battery = recordRx.battery; penData.flags = recordRx.flags; UserError.Log.d(TAG, "Saving Record index: " + penData.index); penData.save(); newRecord = true; gotIndex = (int) penData.index; gotAll = lastIndex == gotIndex; if (InPen.soundsEnabled() && JoH.ratelimit("pen_data_in", 1)) { JoH.playResourceAudio(R.raw.bt_meter_data_in); } lastPenData = penData; } } else { UserError.Log.e(TAG, "Error creating record from: " + HexDump.dumpHexString(record)); } } } if (newRecord) { Inevitable.task("process-inpen-data", 1000, ProcessPenData::process); } } private void keepAlive() { I.connection.writeCharacteristic(KEEPALIVE, new KeepAliveTx().getBytes()).subscribe( value -> { UserError.Log.d(TAG, "Sent KeepAlive ok: "); changeNextState(); }, throwable -> { UserError.Log.e(TAG, "Could not write keepAlive " + throwable); }); } private void bondAuthority() { final AdvertRx adv = new AdvertRx().fromBytes(PersistentStore.getBytes(STORE_INPEN_ADVERT + I.address)); if (adv != null) { final float bondUnits = JoH.roundFloat((float) Pref.getStringToDouble("inpen_prime_units", DEFAULT_BOND_UNITS), 1); final BondTx btx = new BondTx(bondUnits, adv.getFlagBytes()); I.connection.writeCharacteristic(BONDCONTROL, btx.getBytes()).subscribe( value -> { UserError.Log.d(TAG, "Sent BondAuthority ok: " + bytesToHex(value)); changeNextState(); }, throwable -> { if (isErrorResponse(throwable)) { // user dialed up the wrong number of units! err("Cannot bond with pen as incorrect number of units dialed up for pairing. Should be " + bondUnits + " or other error"); bondAsRequired(false); } else { UserError.Log.e(TAG, "Could not write BondAuthority " + throwable); } }); } else { err("Cannot find valid scan record for: " + I.address); } } @TargetApi(Build.VERSION_CODES.KITKAT) private void bondAsRequired(final boolean wait) { final BluetoothDevice device = I.bleDevice.getBluetoothDevice(); final int bondState = device.getBondState(); if (bondState == BOND_NONE) { final boolean bondResultCode = device.createBond(); UserError.Log.d(TAG, "Attempted create bond: result: " + bondResultCode); } else { UserError.Log.d(TAG, "Device is already in bonding state: " + Helper.bondStateToString(bondState)); } if (wait) { for (int c = 0; c < 10; c++) { if (device.getBondState() == BOND_BONDED) { UserError.Log.d(TAG, "Bond created!"); changeNextState(); break; } else { UserError.Log.d(TAG, "Sleeping waiting for bond: " + c); JoH.threadSleep(1000); } } } } public void tryGattRefresh() { if (JoH.ratelimit("inpen-gatt-refresh", 60)) { if (Pref.getBoolean("use_gatt_refresh", true)) { try { if (I.connection != null) UserError.Log.d(TAG, "Trying gatt refresh queue"); I.connection.queue((new GattRefreshOperation(0))).timeout(2, TimeUnit.SECONDS).subscribe( readValue -> { UserError.Log.d(TAG, "Refresh OK: " + readValue); }, throwable -> { UserError.Log.d(TAG, "Refresh exception: " + throwable); }); } catch (NullPointerException e) { UserError.Log.d(TAG, "Probably harmless gatt refresh exception: " + e); } catch (Exception e) { UserError.Log.d(TAG, "Got exception trying gatt refresh: " + e); } } else { UserError.Log.d(TAG, "Gatt refresh rate limited"); } } } /////////////// static final List<UUID> huntCharacterstics = new ArrayList<>(); static { huntCharacterstics.add(Constants.BATTERY); // specimen TODO improve } @Override protected void onServicesDiscovered(RxBleDeviceServices services) { boolean found = false; super.onServicesDiscovered(services); for (BluetoothGattService service : services.getBluetoothGattServices()) { if (D) UserError.Log.d(TAG, "Service: " + getUUIDName(service.getUuid())); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { if (D) UserError.Log.d(TAG, "-- Character: " + getUUIDName(characteristic.getUuid())); for (final UUID check : huntCharacterstics) { if (characteristic.getUuid().equals(check)) { I.readCharacteristic = check; found = true; } } } } if (found) { I.isDiscoveryComplete = true; I.discoverOnce = true; loadInfos(); changeState(mState.next()); } else { UserError.Log.e(TAG, "Could not find characteristic during service discovery. This is very unusual"); tryGattRefresh(); } } private boolean isErrorResponse(final Object throwable) { return throwable instanceof BleGattCharacteristicException && ((BleGattException) throwable).getStatus() == 1; } private void setFailOverTimer() { if (shouldServiceRun()) { if (quietratelimit("inpen-failover-cooldown", 30)) { final long retry_in = MINUTE_IN_MS * 45; UserError.Log.d(TAG, "setFailOverTimer: Restarting in: " + (retry_in / SECOND_IN_MS) + " seconds"); serviceFailoverIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), INPEN_SERVICE_FAILOVER_ID, "failover"); failover_time = JoH.wakeUpIntent(this, retry_in, serviceFailoverIntent); } } else { UserError.Log.d(TAG, "Not setting retry timer as service should not be running"); } } private static boolean shouldServiceRun() { return InPenEntry.isEnabled(); } private static void msg(final String msg) { lastState = msg + " " + hourMinuteString(); lastStateUpdated = JoH.tsl(); } private void err(final String msg) { lastError = msg + " " + hourMinuteString(); UserError.Log.wtf(TAG, msg); } // data for MegaStatus public static List<StatusItem> megaStatus() { final List<StatusItem> l = new ArrayList<>(); if (lastError != null) { l.add(new StatusItem("Last Error", lastError, BAD)); } if (isStarted()) { l.add(new StatusItem("Service Running", JoH.niceTimeScalar(msSince(InPenEntry.started_at)))); l.add(new StatusItem("Brain State", lastState)); if (needsAuthentication) { l.add(new StatusItem("Authentication", "Required", BAD)); } } else { l.add(new StatusItem("Service Stopped", "Not running")); } if (lastReceivedData != -1) { l.add(new StatusItem("Last Connected", dateTimeText(lastReceivedData))); } // TODO remaining records! if (lastPenData != null) { l.add(new StatusItem("Last record", lastPenData.brief(), gotAll ? GOOD : NORMAL)); } if (lastBattery != -1) { l.add(new StatusItem("Battery", lastBattery + "%")); } for (final UUID uuid : INFO_CHARACTERISTICS) { addStatusForCharacteristic(l, getCharactersticName(uuid.toString()), uuid); } if ((currentPenAttachTime != null) && (currentPenTime != null)) { l.add(new StatusItem("Epoch time", dateTimeText(currentPenTime.getPenEpoch()))); l.add(new StatusItem("Attach time", dateTimeText(currentPenTime.fromPenTime(currentPenAttachTime.getPenTime())))); } // return l; } private static void addStatusForCharacteristic(List<StatusItem> l, String name, UUID characteristic) { String result = null; if (Arrays.asList(PRINTABLE_INFO_CHARACTERISTICS).contains(characteristic)) { result = getCharacteristicString(characteristic); } else if (Arrays.asList(HEXDUMP_INFO_CHARACTERISTICS).contains(characteristic)) { result = getCharacteristicHexString(characteristic); } if (result != null) { l.add(new StatusItem(name, result)); } } private static String getCharacteristicString(final UUID uuid) { if (staticCharacteristics == null) return null; final Object objx = staticCharacteristics.get(uuid); if (objx != null) { if (objx instanceof byte[]) { return new String((byte[]) objx); } } return null; } private static String getCharacteristicHexString(final UUID uuid) { if (staticCharacteristics == null) return null; final Object objx = staticCharacteristics.get(uuid); if (objx != null) { if (objx instanceof byte[]) { return JoH.bytesToHex((byte[]) objx); } } return null; } }