/* * The MIT License (MIT) * * Copyright (c) 2013 Alexandre Normand * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package org.glukit.dexcom.sync.tasks; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.inject.Inject; import jssc.SerialPort; import jssc.SerialPortException; import org.glukit.dexcom.sync.DataInputFactory; import org.glukit.dexcom.sync.DataOutputFactory; import org.glukit.dexcom.sync.DatabasePagesPager; import org.glukit.dexcom.sync.ResponseReader; import org.glukit.dexcom.sync.model.*; import org.glukit.dexcom.sync.requests.*; import org.glukit.dexcom.sync.responses.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import static com.google.common.collect.Lists.newArrayList; import static java.lang.String.format; import static org.glukit.dexcom.sync.DecodingUtils.toHexString; import static org.glukit.dexcom.sync.g4.DexcomG4Constants.*; import static org.glukit.dexcom.sync.model.RecordType.EGVData; import static org.glukit.dexcom.sync.model.RecordType.ManufacturingData; import static org.glukit.dexcom.sync.model.RecordType.UserEventData; /** * Fetches the new data since last sync. * * @author alexandre.normand */ public class FetchNewDataRunner { private static Logger LOGGER = LoggerFactory.getLogger(FetchNewDataRunner.class); private final DataOutputFactory dataOutputFactory; private final DataInputFactory dataInputFactory; private final ResponseReader responseReader; @Inject public FetchNewDataRunner(DataOutputFactory dataOutputFactory, DataInputFactory dataInputFactory, ResponseReader responseReader) { this.dataOutputFactory = dataOutputFactory; this.dataInputFactory = dataInputFactory; this.responseReader = responseReader; } /** * Fetches the data from the dexcom * * @param serialPort the serial port of the dexcom receiver * @param since unused at the moment, this optimization might come later * @return the synced data, it's the whole thing of what's still in the receiver memory. */ public DexcomSyncData fetchData(final SerialPort serialPort, final Instant since) { try { serialPort.openPort(); if (!serialPort.isOpened()) { throw new RuntimeException("Can't open receiver to get the data."); } LOGGER.info(format("Opened port [%s]: %b", serialPort.getPortName(), serialPort.isOpened())); serialPort.setParams(FIRMWARE_BAUD_RATE, DATA_BITS, STOP_BITS, NO_PARITY); final ManufacturingParameters manufacturingData = getManufacturingData(serialPort); final long sinceRelativeToDexcomEpoch = since.getEpochSecond() - DEXCOM_EPOCH.getEpochSecond(); final List<GlucoseReadRecord> glucoseReads = getGlucoseReadsSince(serialPort, sinceRelativeToDexcomEpoch); List<UserEventRecord> userEvents = getUserEventRecordsSince(serialPort, sinceRelativeToDexcomEpoch); return new DexcomSyncData(glucoseReads, userEvents, manufacturingData); } catch (Throwable e) { if (serialPort.isOpened()) { try { LOGGER.debug(format("Closing port %s", serialPort.getPortName())); serialPort.closePort(); } catch (SerialPortException se) { LOGGER.warn("Error closing port, ignoring.", se); } } throw Throwables.propagate(e); } } private List<UserEventRecord> getUserEventRecordsSince(SerialPort serialPort, final long sinceRelativeToDexcomEpoch) throws SerialPortException { return newArrayList(Collections2.filter(getUserEvents(serialPort), new Predicate<UserEventRecord>() { @Override public boolean apply(@Nullable UserEventRecord input) { return input.getInternalSecondsSinceDexcomEpoch() > sinceRelativeToDexcomEpoch; } })); } private List<GlucoseReadRecord> getGlucoseReadsSince(SerialPort serialPort, final long sinceRelativeToDexcomEpoch) throws SerialPortException { return newArrayList(Collections2.filter(getGlucoseReads(serialPort), new Predicate<GlucoseReadRecord>() { @Override public boolean apply(@Nullable GlucoseReadRecord input) { return input.getInternalSecondsSinceDexcomEpoch() > sinceRelativeToDexcomEpoch; } })); } private ManufacturingParameters getManufacturingData(SerialPort serialPort) throws SerialPortException { ManufacturingParameters manufacturingData = null; DatabasePagesPager manufacturingDataPager = getPagerForRecordType(serialPort, ManufacturingData); for (DatabaseReadRequestSpec readRequestSpec : manufacturingDataPager) { ManufacturingDataDatabasePagesResponse manufacturingDataDbResponse = readDatabasePage(ManufacturingDataDatabasePagesResponse.class, serialPort, readRequestSpec, ManufacturingData); // We're assuming we'll always have just one or that the most recent is always going to be the one // we want to keep. List<ManufacturingParameters> manufacturingParameters = manufacturingDataDbResponse.getManufacturingParameters(); if (!manufacturingParameters.isEmpty()) { manufacturingData = manufacturingParameters.iterator().next(); } } return manufacturingData; } private DatabasePagesPager getPagerForRecordType(SerialPort serialPort, RecordType recordType) throws SerialPortException { PageRangeResponse pageRange = readManufacturingDataPageRange(serialPort, recordType); return new DatabasePagesPager(pageRange.getFirstPage(), pageRange.getLastPage()); } private <T extends DatabasePagesResponse> T readDatabasePage(Class<T> responseClass, SerialPort serialPort, DatabaseReadRequestSpec readRequestSpec, RecordType recordType) throws SerialPortException { ReadDatabasePagesCommand readDatabasePagesCommand = new ReadDatabasePagesCommand(this.dataOutputFactory, recordType, readRequestSpec.getStartPage(), readRequestSpec.getNumberOfPages()); byte[] packet = readDatabasePagesCommand.asBytes(); LOGGER.info(format("Sending read database pages for %s: %s", recordType.name(), toHexString(packet))); serialPort.writeBytes(packet); return this.responseReader.read(responseClass, serialPort); } private PageRangeResponse readManufacturingDataPageRange(SerialPort serialPort, RecordType recordType) throws SerialPortException { ReadDatabasePageRange readDatabasePageRange = new ReadDatabasePageRange(this.dataOutputFactory, recordType); byte[] packet = readDatabasePageRange.asBytes(); LOGGER.info(format("Sending read database page range for %s: %s", recordType.name(), toHexString(packet))); serialPort.writeBytes(packet); PageRangeResponse pageRangeResponse = this.responseReader.read(PageRangeResponse.class, serialPort); LOGGER.info(format("Page range for %s: [%d] to [%d]", recordType.name(), pageRangeResponse.getFirstPage(), pageRangeResponse.getLastPage())); return pageRangeResponse; } private List<GlucoseReadRecord> getGlucoseReads(SerialPort serialPort) throws SerialPortException { List<GlucoseReadRecord> fullGlucoseReadRecords = newArrayList(); DatabasePagesPager manufacturingDataPager = getPagerForRecordType(serialPort, EGVData); for (DatabaseReadRequestSpec readRequestSpec : manufacturingDataPager) { GlucoseReadsDatabasePagesResponse glucoseReadResponse = readDatabasePage(GlucoseReadsDatabasePagesResponse.class, serialPort, readRequestSpec, EGVData); List<GlucoseReadRecord> glucoseReadRecords = glucoseReadResponse.getRecords(); fullGlucoseReadRecords.addAll(glucoseReadRecords); } return fullGlucoseReadRecords; } private List<UserEventRecord> getUserEvents(SerialPort serialPort) throws SerialPortException { List<UserEventRecord> allUserEvents = newArrayList(); DatabasePagesPager manufacturingDataPager = getPagerForRecordType(serialPort, UserEventData); for (DatabaseReadRequestSpec readRequestSpec : manufacturingDataPager) { UserEventsDatabasePagesResponse userEventRecordPage = readDatabasePage(UserEventsDatabasePagesResponse.class, serialPort, readRequestSpec, UserEventData); List<UserEventRecord> glucoseReadRecords = userEventRecordPage.getRecords(); allUserEvents.addAll(glucoseReadRecords); } return allUserEvents; } private Utf8PayloadGenericResponse readFirmwareHeader(SerialPort serialPort) throws SerialPortException { ReadFirmwareHeader readFirmwareHeader = new ReadFirmwareHeader(this.dataOutputFactory); byte[] packet = readFirmwareHeader.asBytes(); LOGGER.info(format("Sending read firmware header: %s", toHexString(packet))); serialPort.writeBytes(packet); Utf8PayloadGenericResponse utf8PayloadGenericResponse = this.responseReader.read(Utf8PayloadGenericResponse.class, serialPort); LOGGER.info(format("Receiver plugged with firmware: %s", utf8PayloadGenericResponse.asString())); return utf8PayloadGenericResponse; } private PageRangeResponse readGlucosePageRange(SerialPort serialPort) throws SerialPortException { ReadDatabasePageRange readGlucoseReadDatabasePageRange = new ReadDatabasePageRange(this.dataOutputFactory, EGVData); byte[] packet = readGlucoseReadDatabasePageRange.asBytes(); LOGGER.info(format("Sending read database page range for glucose reads: %s", toHexString(packet))); serialPort.writeBytes(packet); PageRangeResponse glucosePageRangeResponse = this.responseReader.read(PageRangeResponse.class, serialPort); LOGGER.info(format("Page range for glucose reads: [%d] to [%d]", glucosePageRangeResponse.getFirstPage(), glucosePageRangeResponse.getLastPage())); return glucosePageRangeResponse; } }