From 5d92368e5b10fb2d01ae25a2730e15cd05a84da6 Mon Sep 17 00:00:00 2001 From: Ben West Date: Sat, 24 May 2014 16:42:13 -0700 Subject: prep for installable module --- dexcom_reader/__init__.py | 0 dexcom_reader/constants.py | 94 ++++++++++++++ dexcom_reader/crc16.py | 37 ++++++ dexcom_reader/database_records.py | 189 ++++++++++++++++++++++++++++ dexcom_reader/packetwriter.py | 49 ++++++++ dexcom_reader/readdata.py | 259 ++++++++++++++++++++++++++++++++++++++ dexcom_reader/util.py | 82 ++++++++++++ 7 files changed, 710 insertions(+) create mode 100644 dexcom_reader/__init__.py create mode 100644 dexcom_reader/constants.py create mode 100644 dexcom_reader/crc16.py create mode 100644 dexcom_reader/database_records.py create mode 100644 dexcom_reader/packetwriter.py create mode 100644 dexcom_reader/readdata.py create mode 100644 dexcom_reader/util.py (limited to 'dexcom_reader') diff --git a/dexcom_reader/__init__.py b/dexcom_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dexcom_reader/constants.py b/dexcom_reader/constants.py new file mode 100644 index 0000000..2f60020 --- /dev/null +++ b/dexcom_reader/constants.py @@ -0,0 +1,94 @@ +import datetime + + +class Error(Exception): + """Base error for dexcom reader.""" + + +class CrcError(Error): + """Failed to CRC properly.""" + + +DEXCOM_G4_USB_VENDOR = 0x22a3 +DEXCOM_G4_USB_PRODUCT = 0x0047 + +BASE_TIME = datetime.datetime(2009, 1, 1) + +NULL = 0 +ACK = 1 +NAK = 2 +INVALID_COMMAND = 3 +INVALID_PARAM = 4 +INCOMPLETE_PACKET_RECEIVED = 5 +RECEIVER_ERROR = 6 +INVALID_MODE = 7 +PING = 10 +READ_FIRMWARE_HEADER = 11 +READ_DATABASE_PARTITION_INFO = 15 +READ_DATABASE_PAGE_RANGE = 16 +READ_DATABASE_PAGES = 17 +READ_DATABASE_PAGE_HEADER = 18 +READ_TRANSMITTER_ID = 25 +WRITE_TRANSMITTER_ID = 26 +READ_LANGUAGE = 27 +WRITE_LANGUAGE = 28 +READ_DISPLAY_TIME_OFFSET = 29 +WRITE_DISPLAY_TIME_OFFSET = 30 +READ_RTC = 31 +RESET_RECEIVER = 32 +READ_BATTERY_LEVEL = 33 +READ_SYSTEM_TIME = 34 +READ_SYSTEM_TIME_OFFSET = 35 +WRITE_SYSTEM_TIME = 36 +READ_GLUCOSE_UNIT = 37 +WRITE_GLUCOSE_UNIT = 38 +READ_BLINDED_MODE = 39 +WRITE_BLINDED_MODE = 40 +READ_CLOCK_MODE = 41 +WRITE_CLOCK_MODE = 42 +READ_DEVICE_MODE = 43 +ERASE_DATABASE = 45 +SHUTDOWN_RECEIVER = 46 +WRITE_PC_PARAMETERS = 47 +READ_BATTERY_STATE = 48 +READ_HARDWARE_BOARD_ID = 49 +READ_FIRMWARE_SETTINGS = 54 +READ_ENABLE_SETUP_WIZARD_FLAG = 55 +READ_SETUP_WIZARD_STATE = 57 +MAX_COMMAND = 59 +MAX_POSSIBLE_COMMAND = 255 + +EGV_VALUE_MASK = 1023 +EGV_DISPLAY_ONLY_MASK = 32768 +EGV_TREND_ARROW_MASK = 15 + +BATTERY_STATES = [None, 'CHARGING', 'NOT_CHARGING', 'NTC_FAULT', 'BAD_BATTERY'] + +RECORD_TYPES = [ + 'MANUFACTURING_DATA', 'FIRMWARE_PARAMETER_DATA', 'PC_SOFTWARE_PARAMETER', + 'SENSOR_DATA', 'EGV_DATA', 'CAL_SET', 'DEVIATION', 'INSERTION_TIME', + 'RECEIVER_LOG_DATA', 'RECEIVER_ERROR_DATA', 'METER_DATA', 'USER_EVENT_DATA', + 'USER_SETTING_DATA', 'MAX_VALUE', +] + +TREND_ARROW_VALUES = [None, 'DOUBLE_UP', 'SINGLE_UP', '45_UP', 'FLAT', + '45_DOWN', 'SINGLE_DOWN', 'DOUBLE_DOWN', 'NOT_COMPUTABLE', + 'OUT_OF_RANGE'] + +SPECIAL_GLUCOSE_VALUES = {0: None, + 1: 'SENSOR_NOT_ACTIVE', + 2: 'MINIMAL_DEVIATION', + 3: 'NO_ANTENNA', + 5: 'SENSOR_NOT_CALIBRATED', + 6: 'COUNTS_DEVIATION', + 9: 'ABSOLUTE_DEVIATION', + 10: 'POWER_DEVIATION', + 12: 'BAD_RF'} + + +LANGUAGES = { + 0: None, + 1033: 'ENGLISH', +} + + diff --git a/dexcom_reader/crc16.py b/dexcom_reader/crc16.py new file mode 100644 index 0000000..0a55332 --- /dev/null +++ b/dexcom_reader/crc16.py @@ -0,0 +1,37 @@ +TABLE = [ + 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, + 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, + 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, + 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, + 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, + 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, + 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, + 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, + 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, + 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, + 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, + 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, + 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, + 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, + 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, + 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, + 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, + 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, + 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, + 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, + 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, + 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, + 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, + 16050, 3793, 7920 +] + + +def crc16(buf, start=None, end=None): + if start is None: + start = 0 + if end is None: + end = len(buf) + num = 0 + for i in range(start, end): + num = ((num<<8)&0xff00) ^ TABLE[((num>>8)&0xff)^ord(buf[i])] + return num & 0xffff diff --git a/dexcom_reader/database_records.py b/dexcom_reader/database_records.py new file mode 100644 index 0000000..bde0c67 --- /dev/null +++ b/dexcom_reader/database_records.py @@ -0,0 +1,189 @@ +import crc16 +import constants +import struct +import util + + +class BaseDatabaseRecord(object): + FORMAT = None + + @classmethod + def _CheckFormat(cls): + if cls.FORMAT is None or not cls.FORMAT: + raise NotImplementedError("Subclasses of %s need to define FORMAT" + % cls.__name__) + + @classmethod + def _ClassFormat(cls): + cls._CheckFormat() + return struct.Struct(cls.FORMAT) + + @classmethod + def _ClassSize(cls): + return cls._ClassFormat().size + + @property + def FMT(self): + self._CheckFormat() + return _ClassFormat() + + @property + def SIZE(self): + return self._ClassSize() + + @property + def crc(self): + return self.data[-1] + + def __init__(self, data, raw_data): + self.raw_data = raw_data + self.data = data + self.check_crc() + + def check_crc(self): + local_crc = self.calculate_crc() + if local_crc != self.crc: + raise constants.CrcError('Could not parse %s' % self.__class__.__name__) + + def dump(self): + return ''.join('\\x%02x' % ord(c) for c in self.raw_data) + + def calculate_crc(self): + return crc16.crc16(self.raw_data[:-2]) + + @classmethod + def Create(cls, data, record_counter): + offset = record_counter * cls._ClassSize() + raw_data = data[offset:offset + cls._ClassSize()] + unpacked_data = cls._ClassFormat().unpack(raw_data) + return cls(unpacked_data, raw_data) + + +class GenericTimestampedRecord(BaseDatabaseRecord): + @property + def system_time(self): + return util.ReceiverTimeToTime(self.data[0]) + + @property + def display_time(self): + return util.ReceiverTimeToTime(self.data[1]) + + +class GenericXMLRecord(GenericTimestampedRecord): + FORMAT = ' 6: + toread = abs(data_number-6) + second_read = self.read(toread) + all_data += second_read + total_read += toread + out = second_read + else: + out = '' + suffix = self.read(2) + sent_crc = struct.unpack(' 1590: + raise constants.Error('Invalid packet length') + self.flush() + self.write(packet) + + def WriteCommand(self, command_id, *args, **kwargs): + p = packetwriter.PacketWriter() + p.ComposePacket(command_id, *args, **kwargs) + self.WritePacket(p.PacketString()) + + def GenericReadCommand(self, command_id): + self.WriteCommand(command_id) + return self.readpacket() + + def ReadTransmitterId(self): + return self.GenericReadCommand(constants.READ_TRANSMITTER_ID).data + + def ReadLanguage(self): + lang = self.GenericReadCommand(constants.READ_LANGUAGE).data + return constants.LANGUAGES[struct.unpack('H', lang)[0]] + + def ReadBatteryLevel(self): + level = self.GenericReadCommand(constants.READ_BATTERY_LEVEL).data + return struct.unpack('I', level)[0] + + def ReadBatteryState(self): + state = self.GenericReadCommand(constants.READ_BATTERY_STATE).data + return constants.BATTERY_STATES[ord(state)] + + def ReadRTC(self): + rtc = self.GenericReadCommand(constants.READ_RTC).data + return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0]) + + def ReadSystemTime(self): + rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME).data + return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0]) + + def ReadSystemTimeOffset(self): + rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME_OFFSET).data + return datetime.timedelta(seconds=struct.unpack('i', rtc)[0]) + + def ReadDisplayTimeOffset(self): + rtc = self.GenericReadCommand(constants.READ_DISPLAY_TIME_OFFSET).data + return datetime.timedelta(seconds=struct.unpack('i', rtc)[0]) + + def ReadDisplayTime(self): + return self.ReadSystemTime() + self.ReadDisplayTimeOffset() + + def ReadGlucoseUnit(self): + UNIT_TYPE = (None, 'mg/dL', 'mmol/L') + gu = self.GenericReadCommand(constants.READ_GLUCOSE_UNIT).data + return UNIT_TYPE[ord(gu[0])] + + def ReadClockMode(self): + CLOCK_MODE = (24, 12) + cm = self.GenericReadCommand(constants.READ_CLOCK_MODE).data + return CLOCK_MODE[ord(cm[0])] + + def ReadDeviceMode(self): + return self.GenericReadCommand(constants.READ_DEVICE_MODE).data + + def ReadManufacturingData(self): + data = self.ReadRecords('MANUFACTURING_DATA')[0].xmldata + return ET.fromstring(data) + + def flush(self): + self.port.flush() + + def clear(self): + self.port.flushInput() + self.port.flushOutput() + + def GetFirmwareHeader(self): + i = self.GenericReadCommand(constants.READ_FIRMWARE_HEADER) + return ET.fromstring(i.data) + + def GetFirmwareSettings(self): + i = self.GenericReadCommand(constants.READ_FIRMWARE_SETTINGS) + return ET.fromstring(i.data) + + def DataPartitions(self): + i = self.GenericReadCommand(constants.READ_DATABASE_PARTITION_INFO) + return ET.fromstring(i.data) + + def ReadDatabasePageRange(self, record_type): + record_type_index = constants.RECORD_TYPES.index(record_type) + self.WriteCommand(constants.READ_DATABASE_PAGE_RANGE, + chr(record_type_index)) + packet = self.readpacket() + return struct.unpack('II', packet.data) + + def ReadDatabasePage(self, record_type, page): + record_type_index = constants.RECORD_TYPES.index(record_type) + self.WriteCommand(constants.READ_DATABASE_PAGES, + (chr(record_type_index), struct.pack('I', page), chr(1))) + packet = self.readpacket() + assert ord(packet.command) == 1 + # first index (uint), numrec (uint), record_type (byte), revision (byte), + # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc) + header_format = '<2I2c4IH' + header_data_len = struct.calcsize(header_format) + header = struct.unpack_from(header_format, packet.data) + header_crc = crc16.crc16(packet.data[:header_data_len-2]) + assert header_crc == header[-1] + assert ord(header[2]) == record_type_index + assert header[4] == page + packet_data = packet.data[header_data_len:] + + return self.ParsePage(header, packet_data) + + def GenericRecordYielder(self, header, data, record_type): + for x in range(header[1]): + yield record_type.Create(data, x) + + def ParsePage(self, header, data): + record_type = constants.RECORD_TYPES[ord(header[2])] + generic_parser_map = { + 'USER_EVENT_DATA': database_records.EventRecord, + 'METER_DATA': database_records.MeterRecord, + 'INSERTION_TIME': database_records.InsertionRecord, + 'EGV_DATA': database_records.EGVRecord, + } + xml_parsed = ['PC_SOFTWARE_PARAMETER', 'MANUFACTURING_DATA'] + if record_type in generic_parser_map: + return self.GenericRecordYielder(header, data, + generic_parser_map[record_type]) + elif record_type in xml_parsed: + return [database_records.GenericXMLRecord.Create(data, 0)] + else: + raise NotImplementedError('Parsing of %s has not yet been implemented' + % record_type) + + def ReadRecords(self, record_type): + records = [] + assert record_type in constants.RECORD_TYPES + page_range = self.ReadDatabasePageRange(record_type) + for x in range(page_range[0], page_range[1] or 1): + records.extend(self.ReadDatabasePage(record_type, x)) + return records + + +if __name__ == '__main__': + Dexcom.LocateAndDownload() diff --git a/dexcom_reader/util.py b/dexcom_reader/util.py new file mode 100644 index 0000000..c719092 --- /dev/null +++ b/dexcom_reader/util.py @@ -0,0 +1,82 @@ +import constants +import datetime +import os +import platform +import plistlib +import re +import subprocess + + +def ReceiverTimeToTime(rtime): + return constants.BASE_TIME + datetime.timedelta(seconds=rtime) + + +def linux_find_usbserial(vendor, product): + DEV_REGEX = re.compile('^tty(USB|ACM)[0-9]+$') + for usb_dev_root in os.listdir('/sys/bus/usb/devices'): + device_name = os.path.join('/sys/bus/usb/devices', usb_dev_root) + if not os.path.exists(os.path.join(device_name, 'idVendor')): + continue + idv = open(os.path.join(device_name, 'idVendor')).read().strip() + if idv != vendor: + continue + idp = open(os.path.join(device_name, 'idProduct')).read().strip() + if idp != product: + continue + for root, dirs, files in os.walk(device_name): + for option in dirs + files: + if DEV_REGEX.match(option): + return os.path.join('/dev', option) + + +def osx_find_usbserial(vendor, product): + def recur(v): + if hasattr(v, '__iter__') and 'idVendor' in v and 'idProduct' in v: + if v['idVendor'] == vendor and v['idProduct'] == product: + tmp = v + while True: + if 'IODialinDevice' not in tmp and 'IORegistryEntryChildren' in tmp: + tmp = tmp['IORegistryEntryChildren'] + elif 'IODialinDevice' in tmp: + return tmp['IODialinDevice'] + else: + break + + if type(v) == list: + for x in v: + out = recur(x) + if out is not None: + return out + elif type(v) == dict or issubclass(type(v), dict): + for x in v.values(): + out = recur(x) + if out is not None: + return out + + sp = subprocess.Popen(['/usr/sbin/ioreg', '-k', 'IODialinDevice', + '-r', '-t', '-l', '-a', '-x'], + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, _ = sp.communicate() + plist = plistlib.readPlistFromString(stdout) + return recur(plist) + + +def find_usbserial(vendor, product): + """Find the tty device for a given usbserial devices identifiers. + + Args: + vendor: (int) something like 0x0000 + product: (int) something like 0x0000 + + Returns: + String, like /dev/ttyACM0 or /dev/tty.usb... + """ + if platform.system() == 'Linux': + vendor, product = [('%04x' % (x)).strip() for x in (vendor, product)] + return linux_find_usbserial(vendor, product) + elif platform.system() == 'Darwin': + return osx_find_usbserial(vendor, product) + else: + raise NotImplementedError('Cannot find serial ports on %s' + % platform.system()) -- cgit v1.2.3