package com.nightscout.core.drivers; import com.nightscout.core.dexcom.Command; import com.nightscout.core.dexcom.PacketBuilder; import com.nightscout.core.dexcom.ReadPacket; import com.nightscout.core.dexcom.ReceiverPowerLevel; import com.nightscout.core.dexcom.RecordType; import com.nightscout.core.dexcom.Utils; import com.nightscout.core.dexcom.records.CalRecord; import com.nightscout.core.dexcom.records.EGVRecord; import com.nightscout.core.dexcom.records.GenericTimestampRecord; import com.nightscout.core.dexcom.records.InsertionRecord; import com.nightscout.core.dexcom.records.MeterRecord; import com.nightscout.core.dexcom.records.PageHeader; import com.nightscout.core.dexcom.records.SensorRecord; import net.tribe7.common.base.Optional; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import java.io.IOException; import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; public class ReadData { private static final int IO_TIMEOUT = 25000; private static final int MIN_LEN = 256; protected final Logger log = LoggerFactory.getLogger(this.getClass()); private DeviceTransport mSerialDevice; // Storing this to reduce the number of reads from the device for other attributes private Document manufacturingDataXml; public ReadData(DeviceTransport device) { mSerialDevice = device; } public List<EGVRecord> getRecentEGVs(long rcvrTime, long refTime) throws IOException { int endPage = readDataBasePageRange(RecordType.EGV_DATA); byte[] data = readDataBasePage(RecordType.EGV_DATA, endPage); return parsePage(data, EGVRecord.class, rcvrTime, refTime); } public List<InsertionRecord> getRecentInsertion(long rcvrTime, long refTime) throws IOException { int endPage = readDataBasePageRange(RecordType.INSERTION_TIME); byte[] data = readDataBasePage(RecordType.INSERTION_TIME, endPage); return parsePage(data, InsertionRecord.class, rcvrTime, refTime); } public List<EGVRecord> getRecentEGVsPages(int numOfRecentPages, long rcvrTime, long refTime) throws IOException { if (numOfRecentPages < 1) { throw new IllegalArgumentException("Number of pages must be greater than 1."); } log.debug("Reading EGV page range..."); int endPage = readDataBasePageRange(RecordType.EGV_DATA); log.debug("Reading {} EGV page(s)...", numOfRecentPages); numOfRecentPages = numOfRecentPages - 1; List<EGVRecord> allPages = new ArrayList<>(); for (int i = Math.min(numOfRecentPages, endPage); i >= 0; i--) { int nextPage = endPage - i; log.debug("Reading #{} EGV pages (page number {})", i, nextPage); byte[] data = readDataBasePage(RecordType.EGV_DATA, nextPage); allPages.addAll(parsePage(data, EGVRecord.class, rcvrTime, refTime)); } log.debug("Read complete of EGV pages."); return allPages; } public long getTimeSinceEGVRecord(EGVRecord egvRecord) throws IOException { return readSystemTime() - egvRecord.getRawSystemTimeSeconds(); } public List<MeterRecord> getRecentMeterRecords(long rcvrTime, long refTime) throws IOException { log.debug("Reading Meter page..."); int endPage = readDataBasePageRange(RecordType.METER_DATA); byte[] data = readDataBasePage(RecordType.METER_DATA, endPage); return parsePage(data, MeterRecord.class, rcvrTime, refTime); } public List<SensorRecord> getRecentSensorRecords(int numOfRecentPages, long rcvrTime, long refTime) throws IOException { if (numOfRecentPages < 1) { throw new IllegalArgumentException("Number of pages must be greater than 1."); } log.debug("Reading Sensor page range..."); int endPage = readDataBasePageRange(RecordType.SENSOR_DATA); log.debug("Reading {} Sensor page(s)...", numOfRecentPages); numOfRecentPages = numOfRecentPages - 1; List<SensorRecord> allPages = new ArrayList<>(); for (int i = Math.min(numOfRecentPages, endPage); i >= 0; i--) { int nextPage = endPage - i; log.debug("Reading #{} Sensor pages (page number {})", i, nextPage); byte[] data = readDataBasePage(RecordType.SENSOR_DATA, nextPage); allPages.addAll(parsePage(data, SensorRecord.class, rcvrTime, refTime)); } log.debug("Read complete of Sensor pages."); return allPages; } public List<CalRecord> getRecentCalRecords(long rcvrTime, long refTime) throws IOException { log.debug("Reading Cal Records page range..."); int endPage = readDataBasePageRange(RecordType.CAL_SET); log.debug("Reading Cal Records page..."); byte[] data = readDataBasePage(RecordType.CAL_SET, endPage); return parsePage(data, CalRecord.class, rcvrTime, refTime); } public boolean ping() throws IOException { writeCommand(Command.PING); return read(MIN_LEN).getCommand() == Command.ACK; } public ReceiverPowerLevel getPowerLevel() throws IOException { writeCommand(Command.READ_CHARGER_CURRENT_SETTING); byte[] readData = read(MIN_LEN).getData(); return ReceiverPowerLevel.values()[ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt()]; } public boolean setPowerLevel(ReceiverPowerLevel powerLevel) throws IOException { List<Byte> payload = new ArrayList<>(); payload.add((byte) powerLevel.ordinal()); writeCommand(Command.WRITE_CHARGER_CURRENT_SETTING, payload); return read(MIN_LEN).getCommand() == Command.ACK; } public int readBatteryLevel() throws IOException { log.debug("Reading battery level..."); writeCommand(Command.READ_BATTERY_LEVEL); // byte[] readData = read(MIN_LEN).getData(); byte[] readData = read(10).getData(); return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(); } public String readSerialNumber() throws IOException { return getManufacturingAttribute("SerialNumber").or(""); } public String readTrasmitterId() throws IOException { writeCommand(Command.READ_TRANSMITTER_ID); byte[] data = read(11).getData(); return new String(data); } private Optional<String> getManufacturingAttribute(String attribute) throws IOException { String result = null; if (manufacturingDataXml == null) { byte[] packet = readDataBasePage(RecordType.MANUFACTURING_DATA, 0); String raw = new String(packet); String xml = raw.substring(raw.indexOf('<'), raw.lastIndexOf('>') + 1); try { log.debug("Manufacturing Response size: " + packet.length); log.debug("Manufacturing data: " + xml); log.debug("Manufacturing data: " + new String(packet)); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); manufacturingDataXml = builder.parse(new InputSource(new StringReader(xml))); Element element = manufacturingDataXml.getDocumentElement(); result = element.getAttribute(attribute); } catch (ParserConfigurationException | SAXException e) { throw new IOException("Unable to parse manufacturing data", e); } } else { Element element = manufacturingDataXml.getDocumentElement(); result = element.getAttribute(attribute); } return Optional.fromNullable(result); } public DateTime readDisplayTime() throws IOException { return Utils.receiverTimeToDate(readSystemTime() + readDisplayTimeOffset()); } public long readSystemTime() throws IOException { log.debug("Reading system time..."); writeCommand(Command.READ_SYSTEM_TIME); byte[] readData = read(10).getData(); return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(); } public int readDisplayTimeOffset() throws IOException { log.debug("Reading display time offset..."); writeCommand(Command.READ_DISPLAY_TIME_OFFSET); byte[] readData = read(MIN_LEN).getData(); return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(); } private int readDataBasePageRange(RecordType recordType) throws IOException { ArrayList<Byte> payload = new ArrayList<>(); payload.add((byte) recordType.ordinal()); writeCommand(Command.READ_DATABASE_PAGE_RANGE, payload); byte[] readData = read(14).getData(); log.debug("Length of readData: {}", readData.length); return ByteBuffer.wrap(readData).order(ByteOrder.LITTLE_ENDIAN).getInt(4); } private byte[] readDataBasePage(RecordType recordType, int page) throws IOException { byte numOfPages = 1; if (page < 0) { throw new IllegalArgumentException("Invalid page requested:" + page); } ArrayList<Byte> payload = new ArrayList<>(); payload.add((byte) recordType.ordinal()); byte[] pageInt = ByteBuffer.allocate(4).putInt(page).array(); payload.add(pageInt[3]); payload.add(pageInt[2]); payload.add(pageInt[1]); payload.add(pageInt[0]); payload.add(numOfPages); writeCommand(Command.READ_DATABASE_PAGES, payload); // int expectedSize = (recordType == RecordType.MANUFACTURING_DATA) ? 528 : 534; return read(534).getData(); } private void writeCommand(Command command, List<Byte> payload) throws IOException { byte[] packet = new PacketBuilder(command, payload).build(); if (mSerialDevice != null) { mSerialDevice.write(packet, IO_TIMEOUT); } } protected void writeCommand(Command command) throws IOException { byte[] packet = new PacketBuilder(command).build(); if (mSerialDevice != null) { mSerialDevice.write(packet, IO_TIMEOUT); } } private ReadPacket read(int numOfBytes) throws IOException { byte[] response = new byte[numOfBytes]; int len; // try { response = mSerialDevice.read(numOfBytes, IO_TIMEOUT); len = response.length; log.debug("Read {} byte(s) complete.", len); // Add a 100ms delay for when multiple write/reads are occurring in series // Thread.sleep(100); // TODO: this debug code to print data of the read, should be removed after // finding the source of the reading issue String bytes = ""; int readAmount = len; for (int i = 0; i < readAmount; i++) bytes += String.format("%02x", response[i]) + " "; log.debug("Read data: {}", bytes); //////////////////////////////////////////////////////////////////////////////////////// // } catch (InterruptedException e) { // e.printStackTrace(); // } return new ReadPacket(response); } private <T extends GenericTimestampRecord> List<T> parsePage(byte[] data, Class<T> clazz, long rcvrTime, long refTime) { PageHeader pageHeader = new PageHeader(data); List<T> records = new ArrayList<>(); try { for (int i = 0; i < pageHeader.getNumOfRecords(); i++) { int startIdx; switch (pageHeader.getRecordType()) { case EGV_DATA: startIdx = PageHeader.HEADER_SIZE + (EGVRecord.RECORD_SIZE + 1) * i; records.add(clazz.cast(new EGVRecord(Arrays.copyOfRange(data, startIdx, startIdx + EGVRecord.RECORD_SIZE), rcvrTime, refTime))); break; case CAL_SET: int recordLength = (pageHeader.getRevision() <= 2) ? CalRecord.RECORD_SIZE : CalRecord.RECORD_V2_SIZE; startIdx = PageHeader.HEADER_SIZE + (recordLength + 1) * i; records.add(clazz.cast(new CalRecord(Arrays.copyOfRange(data, startIdx, startIdx + recordLength), rcvrTime, refTime))); break; case METER_DATA: startIdx = PageHeader.HEADER_SIZE + (MeterRecord.RECORD_SIZE + 1) * i; records.add(clazz.cast(new MeterRecord(Arrays.copyOfRange(data, startIdx, startIdx + MeterRecord.RECORD_SIZE), rcvrTime, refTime))); break; case SENSOR_DATA: startIdx = PageHeader.HEADER_SIZE + (SensorRecord.RECORD_SIZE + 1) * i; records.add(clazz.cast(new SensorRecord(Arrays.copyOfRange(data, startIdx, startIdx + SensorRecord.RECORD_SIZE), rcvrTime, refTime))); break; case INSERTION_TIME: startIdx = PageHeader.HEADER_SIZE + (InsertionRecord.RECORD_SIZE + 1) * i; records.add(clazz.cast(new InsertionRecord(Arrays.copyOfRange(data, startIdx, startIdx + InsertionRecord.RECORD_SIZE), rcvrTime, refTime))); break; default: throw new IllegalArgumentException(String.format("Unknown record type: %s", pageHeader.getRecordType().name())); } } } catch (IllegalArgumentException e) { e.printStackTrace(); } return records; } }