├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── dexcom_reader ├── __init__.py ├── constants.py ├── crc16.py ├── database_records.py ├── etc │ └── udev │ │ └── rules.d │ │ └── 80-dexcom.rules ├── packetwriter.py ├── readdata.py ├── record_test.py └── util.py ├── ez_setup.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | group: travis_latest 2 | language: python 3 | cache: pip 4 | matrix: 5 | allow_failures: 6 | - python: 2.7 7 | include: 8 | - python: 2.7 9 | - python: 3.5 10 | dist: xenial # required for Debian Squeeze 11 | #- python: 3.6 12 | - python: 3.7 13 | dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069) 14 | install: 15 | # - pip install -r requirements.txt 16 | - pip install flake8 17 | before_script: 18 | # stop the build if there are Python syntax errors or undefined names 19 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 20 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 21 | - flake8 . --count --ignore=E203 --exit-zero --max-complexity=10 --max-line-length=127 --statistics 22 | script: 23 | - true # add other tests here 24 | notifications: 25 | on_success: change 26 | on_failure: change # `always` will be the setting once code changes slow down 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | 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. 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include CHANGES.* 4 | recursive-include dexcom_reader/etc * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dexcom_reader 2 | ============= 3 | 4 | This is a handful of scripts for dumping data from a Dexcom G4 Glucose Monitor 5 | connected to a computer with USB. 6 | 7 | Out of the box dumps data like: 8 | 9 | % python readdata.py 10 | Found Dexcom G4 Receiver S/N: SMXXXXXXXX 11 | Transmitter paired: 6XXXXX 12 | Battery Status: CHARGING (83%) 13 | Record count: 14 | - Meter records: 340 15 | - CGM records: 3340 16 | - CGM commitable records: 3340 17 | - Event records: 15 18 | - Insertion records: 4 19 | -------------------------------------------------------------------------------- /dexcom_reader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openaps/dexcom_reader/5228d3b06826d393c7c8d284998d427c56ed94af/dexcom_reader/__init__.py -------------------------------------------------------------------------------- /dexcom_reader/constants.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Error(Exception): 5 | """Base error for dexcom reader.""" 6 | 7 | 8 | class CrcError(Error): 9 | """Failed to CRC properly.""" 10 | 11 | 12 | DEXCOM_USB_VENDOR = 0x22A3 13 | DEXCOM_USB_PRODUCT = 0x0047 14 | 15 | BASE_TIME = datetime.datetime(2009, 1, 1) 16 | 17 | NULL = 0 18 | ACK = 1 19 | NAK = 2 20 | INVALID_COMMAND = 3 21 | INVALID_PARAM = 4 22 | INCOMPLETE_PACKET_RECEIVED = 5 23 | RECEIVER_ERROR = 6 24 | INVALID_MODE = 7 25 | PING = 10 26 | READ_FIRMWARE_HEADER = 11 27 | READ_DATABASE_PARTITION_INFO = 15 28 | READ_DATABASE_PAGE_RANGE = 16 29 | READ_DATABASE_PAGES = 17 30 | READ_DATABASE_PAGE_HEADER = 18 31 | READ_TRANSMITTER_ID = 25 32 | WRITE_TRANSMITTER_ID = 26 33 | READ_LANGUAGE = 27 34 | WRITE_LANGUAGE = 28 35 | READ_DISPLAY_TIME_OFFSET = 29 36 | WRITE_DISPLAY_TIME_OFFSET = 30 37 | READ_RTC = 31 38 | RESET_RECEIVER = 32 39 | READ_BATTERY_LEVEL = 33 40 | READ_SYSTEM_TIME = 34 41 | READ_SYSTEM_TIME_OFFSET = 35 42 | WRITE_SYSTEM_TIME = 36 43 | READ_GLUCOSE_UNIT = 37 44 | WRITE_GLUCOSE_UNIT = 38 45 | READ_BLINDED_MODE = 39 46 | WRITE_BLINDED_MODE = 40 47 | READ_CLOCK_MODE = 41 48 | WRITE_CLOCK_MODE = 42 49 | READ_DEVICE_MODE = 43 50 | ERASE_DATABASE = 45 51 | SHUTDOWN_RECEIVER = 46 52 | WRITE_PC_PARAMETERS = 47 53 | READ_BATTERY_STATE = 48 54 | READ_HARDWARE_BOARD_ID = 49 55 | READ_FIRMWARE_SETTINGS = 54 56 | READ_ENABLE_SETUP_WIZARD_FLAG = 55 57 | READ_SETUP_WIZARD_STATE = 57 58 | READ_CHARGER_CURRENT_SETTING = 59 59 | WRITE_CHARGER_CURRENT_SETTING = 60 60 | MAX_COMMAND = 61 61 | MAX_POSSIBLE_COMMAND = 255 62 | 63 | EGV_VALUE_MASK = 1023 64 | EGV_DISPLAY_ONLY_MASK = 32768 65 | EGV_TREND_ARROW_MASK = 15 66 | 67 | BATTERY_STATES = [None, "CHARGING", "NOT_CHARGING", "NTC_FAULT", "BAD_BATTERY"] 68 | 69 | RECORD_TYPES = [ 70 | "MANUFACTURING_DATA", 71 | "FIRMWARE_PARAMETER_DATA", 72 | "PC_SOFTWARE_PARAMETER", 73 | "SENSOR_DATA", 74 | "EGV_DATA", 75 | "CAL_SET", 76 | "DEVIATION", 77 | "INSERTION_TIME", 78 | "RECEIVER_LOG_DATA", 79 | "RECEIVER_ERROR_DATA", 80 | "METER_DATA", 81 | "USER_EVENT_DATA", 82 | "USER_SETTING_DATA", 83 | "MAX_VALUE", 84 | ] 85 | 86 | TREND_ARROW_VALUES = [ 87 | None, 88 | "DOUBLE_UP", 89 | "SINGLE_UP", 90 | "45_UP", 91 | "FLAT", 92 | "45_DOWN", 93 | "SINGLE_DOWN", 94 | "DOUBLE_DOWN", 95 | "NOT_COMPUTABLE", 96 | "OUT_OF_RANGE", 97 | ] 98 | 99 | SPECIAL_GLUCOSE_VALUES = { 100 | 0: None, 101 | 1: "SENSOR_NOT_ACTIVE", 102 | 2: "MINIMAL_DEVIATION", 103 | 3: "NO_ANTENNA", 104 | 5: "SENSOR_NOT_CALIBRATED", 105 | 6: "COUNTS_DEVIATION", 106 | 9: "ABSOLUTE_DEVIATION", 107 | 10: "POWER_DEVIATION", 108 | 12: "BAD_RF", 109 | } 110 | 111 | 112 | LANGUAGES = { 113 | 0: None, 114 | 1033: "ENGLISH", 115 | } 116 | -------------------------------------------------------------------------------- /dexcom_reader/crc16.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | TABLE = [ 3 | 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, 4 | 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 5 | 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, 6 | 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, 7 | 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, 8 | 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, 9 | 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, 10 | 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, 11 | 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, 12 | 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, 13 | 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, 14 | 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 15 | 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 16 | 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, 17 | 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 18 | 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, 19 | 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, 20 | 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, 21 | 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 22 | 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, 23 | 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, 24 | 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 25 | 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 26 | 16050, 3793, 7920 27 | ] 28 | # fmt: on 29 | 30 | 31 | def crc16(buf, start=None, end=None): 32 | if start is None: 33 | start = 0 34 | if end is None: 35 | end = len(buf) 36 | num = 0 37 | for i in range(start, end): 38 | num = ((num << 8) & 0xFF00) ^ TABLE[((num >> 8) & 0xFF) ^ ord(buf[i])] 39 | return num & 0xFFFF 40 | -------------------------------------------------------------------------------- /dexcom_reader/database_records.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import struct 3 | 4 | from . import constants, crc16, util 5 | 6 | 7 | class BaseDatabaseRecord: 8 | FORMAT = None 9 | 10 | @classmethod 11 | def _CheckFormat(cls): 12 | if cls.FORMAT is None or not cls.FORMAT: 13 | raise NotImplementedError( 14 | "Subclasses of %s need to define FORMAT" % cls.__name__ 15 | ) 16 | 17 | @classmethod 18 | def _ClassFormat(cls): 19 | cls._CheckFormat() 20 | return struct.Struct(cls.FORMAT) 21 | 22 | @classmethod 23 | def _ClassSize(cls): 24 | return cls._ClassFormat().size 25 | 26 | @property 27 | def FMT(self): 28 | self._CheckFormat() 29 | return self._ClassFormat() 30 | 31 | @property 32 | def SIZE(self): 33 | return self._ClassSize() 34 | 35 | @property 36 | def crc(self): 37 | return self.data[-1] 38 | 39 | def __init__(self, data, raw_data): 40 | self.raw_data = raw_data 41 | self.data = data 42 | self.check_crc() 43 | 44 | def check_crc(self): 45 | local_crc = self.calculate_crc() 46 | if local_crc != self.crc: 47 | raise constants.CrcError("Could not parse %s" % self.__class__.__name__) 48 | 49 | def dump(self): 50 | return "".join("\\x%02x" % ord(c) for c in self.raw_data) 51 | 52 | def calculate_crc(self): 53 | return crc16.crc16(self.raw_data[:-2]) 54 | 55 | @classmethod 56 | def Create(cls, data, record_counter): 57 | offset = record_counter * cls._ClassSize() 58 | raw_data = data[offset : offset + cls._ClassSize()] 59 | unpacked_data = cls._ClassFormat().unpack(raw_data) 60 | return cls(unpacked_data, raw_data) 61 | 62 | 63 | class GenericTimestampedRecord(BaseDatabaseRecord): 64 | FIELDS = [] 65 | BASE_FIELDS = ["system_time", "display_time"] 66 | 67 | @property 68 | def system_time(self): 69 | return util.ReceiverTimeToTime(self.data[0]) 70 | 71 | @property 72 | def display_time(self): 73 | return util.ReceiverTimeToTime(self.data[1]) 74 | 75 | def to_dict(self): 76 | d = dict() 77 | for k in self.BASE_FIELDS + self.FIELDS: 78 | d[k] = getattr(self, k) 79 | if callable(getattr(d[k], "isoformat", None)): 80 | d[k] = d[k].isoformat() 81 | return d 82 | 83 | 84 | class GenericXMLRecord(GenericTimestampedRecord): 85 | FORMAT = " 6: 94 | toread = abs(data_number - 6) 95 | second_read = self.read(toread) 96 | all_data += second_read 97 | total_read += toread 98 | out = second_read 99 | else: 100 | out = "" 101 | suffix = self.read(2) 102 | sent_crc = struct.unpack(" 1590: 121 | raise constants.Error("Invalid packet length") 122 | self.flush() 123 | self.write(packet) 124 | 125 | def WriteCommand(self, command_id, *args, **kwargs): 126 | p = packetwriter.PacketWriter() 127 | p.ComposePacket(command_id, *args, **kwargs) 128 | self.WritePacket(p.PacketString()) 129 | 130 | def GenericReadCommand(self, command_id): 131 | self.WriteCommand(command_id) 132 | return self.readpacket() 133 | 134 | def ReadTransmitterId(self): 135 | return self.GenericReadCommand(constants.READ_TRANSMITTER_ID).data 136 | 137 | def ReadLanguage(self): 138 | lang = self.GenericReadCommand(constants.READ_LANGUAGE).data 139 | return constants.LANGUAGES[struct.unpack("H", lang)[0]] 140 | 141 | def ReadBatteryLevel(self): 142 | level = self.GenericReadCommand(constants.READ_BATTERY_LEVEL).data 143 | return struct.unpack("I", level)[0] 144 | 145 | def ReadBatteryState(self): 146 | state = self.GenericReadCommand(constants.READ_BATTERY_STATE).data 147 | return constants.BATTERY_STATES[ord(state)] 148 | 149 | def ReadRTC(self): 150 | rtc = self.GenericReadCommand(constants.READ_RTC).data 151 | return util.ReceiverTimeToTime(struct.unpack("I", rtc)[0]) 152 | 153 | def ReadSystemTime(self): 154 | rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME).data 155 | return util.ReceiverTimeToTime(struct.unpack("I", rtc)[0]) 156 | 157 | def ReadSystemTimeOffset(self): 158 | raw = self.GenericReadCommand(constants.READ_SYSTEM_TIME_OFFSET).data 159 | return datetime.timedelta(seconds=struct.unpack("i", raw)[0]) 160 | 161 | def ReadDisplayTimeOffset(self): 162 | raw = self.GenericReadCommand(constants.READ_DISPLAY_TIME_OFFSET).data 163 | return datetime.timedelta(seconds=struct.unpack("i", raw)[0]) 164 | 165 | def WriteDisplayTimeOffset(self, offset=None): 166 | payload = struct.pack("i", offset) 167 | self.WriteCommand(constants.WRITE_DISPLAY_TIME_OFFSET, payload) 168 | packet = self.readpacket() 169 | return dict(ACK=ord(packet.command) == constants.ACK) 170 | 171 | def ReadDisplayTime(self): 172 | return self.ReadSystemTime() + self.ReadDisplayTimeOffset() 173 | 174 | def ReadGlucoseUnit(self): 175 | UNIT_TYPE = (None, "mg/dL", "mmol/L") 176 | gu = self.GenericReadCommand(constants.READ_GLUCOSE_UNIT).data 177 | return UNIT_TYPE[ord(gu[0])] 178 | 179 | def ReadClockMode(self): 180 | CLOCK_MODE = (24, 12) 181 | cm = self.GenericReadCommand(constants.READ_CLOCK_MODE).data 182 | return CLOCK_MODE[ord(cm[0])] 183 | 184 | def ReadDeviceMode(self): 185 | # ??? 186 | return self.GenericReadCommand(constants.READ_DEVICE_MODE).data 187 | 188 | def ReadBlindedMode(self): 189 | MODES = {0: False} 190 | raw = self.GenericReadCommand(constants.READ_BLINDED_MODE).data 191 | mode = MODES.get(bytearray(raw)[0], True) 192 | return mode 193 | 194 | def ReadHardwareBoardId(self): 195 | return self.GenericReadCommand(constants.READ_HARDWARE_BOARD_ID).data 196 | 197 | def ReadEnableSetupWizardFlag(self): 198 | # ??? 199 | return self.GenericReadCommand(constants.READ_ENABLE_SETUP_WIZARD_FLAG).data 200 | 201 | def ReadSetupWizardState(self): 202 | # ??? 203 | return self.GenericReadCommand(constants.READ_SETUP_WIZARD_STATE).data 204 | 205 | def WriteChargerCurrentSetting(self, status): 206 | MAP = ("Off", "Power100mA", "Power500mA", "PowerMax", "PowerSuspended") 207 | payload = str(bytearray([MAP.index(status)])) 208 | self.WriteCommand(constants.WRITE_CHARGER_CURRENT_SETTING, payload) 209 | packet = self.readpacket() 210 | raw = bytearray(packet.data) 211 | return dict(ACK=ord(packet.command) == constants.ACK, raw=list(raw)) 212 | 213 | def ReadChargerCurrentSetting(self): 214 | MAP = ("Off", "Power100mA", "Power500mA", "PowerMax", "PowerSuspended") 215 | raw = bytearray( 216 | self.GenericReadCommand(constants.READ_CHARGER_CURRENT_SETTING).data 217 | ) 218 | return MAP[raw[0]] 219 | 220 | def ReadManufacturingData(self): 221 | data = self.ReadRecords("MANUFACTURING_DATA")[0].xmldata 222 | return ET.fromstring(data) 223 | 224 | def flush(self): 225 | self.port.flush() 226 | 227 | def clear(self): 228 | self.port.flushInput() 229 | self.port.flushOutput() 230 | 231 | def GetFirmwareHeader(self): 232 | i = self.GenericReadCommand(constants.READ_FIRMWARE_HEADER) 233 | return ET.fromstring(i.data) 234 | 235 | def GetFirmwareSettings(self): 236 | i = self.GenericReadCommand(constants.READ_FIRMWARE_SETTINGS) 237 | return ET.fromstring(i.data) 238 | 239 | def DataPartitions(self): 240 | i = self.GenericReadCommand(constants.READ_DATABASE_PARTITION_INFO) 241 | return ET.fromstring(i.data) 242 | 243 | def ReadDatabasePageRange(self, record_type): 244 | record_type_index = constants.RECORD_TYPES.index(record_type) 245 | self.WriteCommand(constants.READ_DATABASE_PAGE_RANGE, chr(record_type_index)) 246 | packet = self.readpacket() 247 | return struct.unpack("II", packet.data) 248 | 249 | def ReadDatabasePage(self, record_type, page): 250 | record_type_index = constants.RECORD_TYPES.index(record_type) 251 | self.WriteCommand( 252 | constants.READ_DATABASE_PAGES, 253 | (chr(record_type_index), struct.pack("I", page), chr(1)), 254 | ) 255 | packet = self.readpacket() 256 | assert ord(packet.command) == 1 257 | # first index (uint), numrec (uint), record_type (byte), revision (byte), 258 | # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc) 259 | header_format = "<2IcB4IH" 260 | header_data_len = struct.calcsize(header_format) 261 | header = struct.unpack_from(header_format, packet.data) 262 | header_crc = crc16.crc16(packet.data[: header_data_len - 2]) 263 | assert header_crc == header[-1] 264 | assert ord(header[2]) == record_type_index 265 | assert header[4] == page 266 | packet_data = packet.data[header_data_len:] 267 | 268 | return self.ParsePage(header, packet_data) 269 | 270 | def GenericRecordYielder(self, header, data, record_type): 271 | for x in range(header[1]): 272 | yield record_type.Create(data, x) 273 | 274 | PARSER_MAP = { 275 | "USER_EVENT_DATA": database_records.EventRecord, 276 | "METER_DATA": database_records.MeterRecord, 277 | "CAL_SET": database_records.Calibration, 278 | "INSERTION_TIME": database_records.InsertionRecord, 279 | "EGV_DATA": database_records.EGVRecord, 280 | "SENSOR_DATA": database_records.SensorRecord, 281 | } 282 | 283 | def ParsePage(self, header, data): 284 | record_type = constants.RECORD_TYPES[ord(header[2])] 285 | revision = int(header[3]) 286 | generic_parser_map = self.PARSER_MAP 287 | if revision > 4 and record_type == "EGV_DATA": 288 | generic_parser_map.update(EGV_DATA=database_records.G6EGVRecord) 289 | if revision > 1 and record_type == "INSERTION_TIME": 290 | generic_parser_map.update(INSERTION_TIME=database_records.G5InsertionRecord) 291 | if revision > 2 and record_type == "METER_DATA": 292 | generic_parser_map.update(METER_DATA=database_records.G5MeterRecord) 293 | if revision < 2 and record_type == "CAL_SET": 294 | generic_parser_map.update(CAL_SET=database_records.LegacyCalibration) 295 | xml_parsed = ["PC_SOFTWARE_PARAMETER", "MANUFACTURING_DATA"] 296 | if record_type in generic_parser_map: 297 | return self.GenericRecordYielder( 298 | header, data, generic_parser_map[record_type] 299 | ) 300 | elif record_type in xml_parsed: 301 | return [database_records.GenericXMLRecord.Create(data, 0)] 302 | else: 303 | raise NotImplementedError( 304 | "Parsing of %s has not yet been implemented" % record_type 305 | ) 306 | 307 | def iter_records(self, record_type): 308 | assert record_type in constants.RECORD_TYPES 309 | page_range = self.ReadDatabasePageRange(record_type) 310 | start, end = page_range 311 | if start != end or not end: 312 | end += 1 313 | for x in reversed(range(start, end)): 314 | records = list(self.ReadDatabasePage(record_type, x)) 315 | records.reverse() 316 | yield from records 317 | 318 | def ReadRecords(self, record_type): 319 | records = [] 320 | assert record_type in constants.RECORD_TYPES 321 | page_range = self.ReadDatabasePageRange(record_type) 322 | start, end = page_range 323 | if start != end or not end: 324 | end += 1 325 | for x in range(start, end): 326 | records.extend(self.ReadDatabasePage(record_type, x)) 327 | return records 328 | 329 | 330 | class DexcomG5(Dexcom): 331 | PARSER_MAP = { 332 | "USER_EVENT_DATA": database_records.EventRecord, 333 | "METER_DATA": database_records.G5MeterRecord, 334 | "CAL_SET": database_records.Calibration, 335 | "INSERTION_TIME": database_records.G5InsertionRecord, 336 | "EGV_DATA": database_records.G5EGVRecord, 337 | "SENSOR_DATA": database_records.SensorRecord, 338 | } 339 | 340 | 341 | class DexcomG6(Dexcom): 342 | PARSER_MAP = { 343 | "USER_EVENT_DATA": database_records.EventRecord, 344 | "METER_DATA": database_records.G5MeterRecord, 345 | "CAL_SET": database_records.Calibration, 346 | "INSERTION_TIME": database_records.G5InsertionRecord, 347 | "EGV_DATA": database_records.G6EGVRecord, 348 | "SENSOR_DATA": database_records.SensorRecord, 349 | } 350 | 351 | 352 | def GetDevice(port, G5=False, G6=False): 353 | if G5: 354 | return DexcomG5(port) 355 | if G6: 356 | return DexcomG6(port) 357 | return Dexcom(port) 358 | 359 | 360 | if __name__ == "__main__": 361 | Dexcom.LocateAndDownload() 362 | -------------------------------------------------------------------------------- /dexcom_reader/record_test.py: -------------------------------------------------------------------------------- 1 | from . import readdata 2 | 3 | dd = readdata.Dexcom.FindDevice() 4 | dr = readdata.Dexcom(dd) 5 | meter_records = dr.ReadRecords("METER_DATA") 6 | print("First Meter Record = ") 7 | print(meter_records[0]) 8 | print("Last Meter Record =") 9 | print(meter_records[-1]) 10 | insertion_records = dr.ReadRecords("INSERTION_TIME") 11 | print("First Insertion Record = ") 12 | print(insertion_records[0]) 13 | print("Last Insertion Record = ") 14 | print(insertion_records[-1]) 15 | -------------------------------------------------------------------------------- /dexcom_reader/util.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import platform 4 | import plistlib 5 | import re 6 | import subprocess 7 | 8 | from . import constants 9 | 10 | 11 | def ReceiverTimeToTime(rtime): 12 | return constants.BASE_TIME + datetime.timedelta(seconds=rtime) 13 | 14 | 15 | def linux_find_usbserial(vendor, product): 16 | DEV_REGEX = re.compile("^tty(USB|ACM)[0-9]+$") 17 | for usb_dev_root in os.listdir("/sys/bus/usb/devices"): 18 | device_name = os.path.join("/sys/bus/usb/devices", usb_dev_root) 19 | if not os.path.exists(os.path.join(device_name, "idVendor")): 20 | continue 21 | idv = open(os.path.join(device_name, "idVendor")).read().strip() 22 | if idv != vendor: 23 | continue 24 | idp = open(os.path.join(device_name, "idProduct")).read().strip() 25 | if idp != product: 26 | continue 27 | for root, dirs, files in os.walk(device_name): 28 | for option in dirs + files: 29 | if DEV_REGEX.match(option): 30 | return os.path.join("/dev", option) 31 | 32 | 33 | def osx_find_usbserial(vendor, product): # noqa: C901 34 | def recur(v): 35 | if hasattr(v, "__iter__") and "idVendor" in v and "idProduct" in v: 36 | if v["idVendor"] == vendor and v["idProduct"] == product: 37 | tmp = v 38 | while True: 39 | if "IODialinDevice" not in tmp and "IORegistryEntryChildren" in tmp: 40 | tmp = tmp["IORegistryEntryChildren"] 41 | elif "IODialinDevice" in tmp: 42 | return tmp["IODialinDevice"] 43 | else: 44 | break 45 | 46 | if isinstance(v, list): 47 | for x in v: 48 | out = recur(x) 49 | if out is not None: 50 | return out 51 | elif isinstance(v, dict) or issubclass(type(v), dict): 52 | for x in list(v.values()): 53 | out = recur(x) 54 | if out is not None: 55 | return out 56 | 57 | sp = subprocess.Popen( 58 | ["/usr/sbin/ioreg", "-k", "IODialinDevice", "-r", "-t", "-l", "-a", "-x"], 59 | stdout=subprocess.PIPE, 60 | stdin=subprocess.PIPE, 61 | stderr=subprocess.PIPE, 62 | ) 63 | stdout, _ = sp.communicate() 64 | plist = plistlib.readPlistFromString(stdout) 65 | return recur(plist) 66 | 67 | 68 | def find_usbserial(vendor, product): 69 | """Find the tty device for a given usbserial devices identifiers. 70 | 71 | Args: 72 | vendor: (int) something like 0x0000 73 | product: (int) something like 0x0000 74 | 75 | Returns: 76 | String, like /dev/ttyACM0 or /dev/tty.usb... 77 | """ 78 | if platform.system() == "Linux": 79 | vendor, product = [("%04x" % (x)).strip() for x in (vendor, product)] 80 | return linux_find_usbserial(vendor, product) 81 | elif platform.system() == "Darwin": 82 | return osx_find_usbserial(vendor, product) 83 | else: 84 | raise NotImplementedError("Cannot find serial ports on %s" % platform.system()) 85 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap setuptools installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import optparse 17 | import os 18 | import platform 19 | import shutil 20 | import subprocess 21 | import sys 22 | import tarfile 23 | import tempfile 24 | from distutils import log 25 | 26 | try: 27 | from site import USER_SITE 28 | except ImportError: 29 | USER_SITE = None 30 | 31 | DEFAULT_VERSION = "1.0" 32 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" 33 | 34 | 35 | def _python_cmd(*args): 36 | args = (sys.executable,) + args 37 | return subprocess.call(args) == 0 38 | 39 | 40 | def _check_call_py24(cmd, *args, **kwargs): 41 | res = subprocess.call(cmd, *args, **kwargs) 42 | 43 | class CalledProcessError(Exception): 44 | pass 45 | 46 | if not res == 0: 47 | msg = "Command '%s' return non-zero exit status %d" % (cmd, res) 48 | raise CalledProcessError(msg) 49 | 50 | 51 | vars(subprocess).setdefault("check_call", _check_call_py24) 52 | 53 | 54 | def _install(tarball, install_args=()): 55 | # extracting the tarball 56 | tmpdir = tempfile.mkdtemp() 57 | log.warn("Extracting in %s", tmpdir) 58 | old_wd = os.getcwd() 59 | try: 60 | os.chdir(tmpdir) 61 | tar = tarfile.open(tarball) 62 | _extractall(tar) 63 | tar.close() 64 | 65 | # going in the directory 66 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 67 | os.chdir(subdir) 68 | log.warn("Now working in %s", subdir) 69 | 70 | # installing 71 | log.warn("Installing Setuptools") 72 | if not _python_cmd("setup.py", "install", *install_args): 73 | log.warn("Something went wrong during the installation.") 74 | log.warn("See the error message above.") 75 | # exitcode will be 2 76 | return 2 77 | finally: 78 | os.chdir(old_wd) 79 | shutil.rmtree(tmpdir) 80 | 81 | 82 | def _build_egg(egg, tarball, to_dir): 83 | # extracting the tarball 84 | tmpdir = tempfile.mkdtemp() 85 | log.warn("Extracting in %s", tmpdir) 86 | old_wd = os.getcwd() 87 | try: 88 | os.chdir(tmpdir) 89 | tar = tarfile.open(tarball) 90 | _extractall(tar) 91 | tar.close() 92 | 93 | # going in the directory 94 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 95 | os.chdir(subdir) 96 | log.warn("Now working in %s", subdir) 97 | 98 | # building an egg 99 | log.warn("Building a Setuptools egg in %s", to_dir) 100 | _python_cmd("setup.py", "-q", "bdist_egg", "--dist-dir", to_dir) 101 | 102 | finally: 103 | os.chdir(old_wd) 104 | shutil.rmtree(tmpdir) 105 | # returning the result 106 | log.warn(egg) 107 | if not os.path.exists(egg): 108 | raise OSError("Could not build the egg.") 109 | 110 | 111 | def _do_download(version, download_base, to_dir, download_delay): 112 | egg = os.path.join( 113 | to_dir, 114 | "setuptools-%s-py%d.%d.egg" 115 | % (version, sys.version_info[0], sys.version_info[1]), 116 | ) 117 | if not os.path.exists(egg): 118 | tarball = download_setuptools(version, download_base, to_dir, download_delay) 119 | _build_egg(egg, tarball, to_dir) 120 | sys.path.insert(0, egg) 121 | 122 | # Remove previously-imported pkg_resources if present (see 123 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). 124 | if "pkg_resources" in sys.modules: 125 | del sys.modules["pkg_resources"] 126 | 127 | import setuptools 128 | 129 | setuptools.bootstrap_install_from = egg 130 | 131 | 132 | def use_setuptools( 133 | version=DEFAULT_VERSION, 134 | download_base=DEFAULT_URL, 135 | to_dir=os.curdir, 136 | download_delay=15, 137 | ): 138 | # making sure we use the absolute path 139 | to_dir = os.path.abspath(to_dir) 140 | was_imported = "pkg_resources" in sys.modules or "setuptools" in sys.modules 141 | try: 142 | import pkg_resources 143 | except ImportError: 144 | return _do_download(version, download_base, to_dir, download_delay) 145 | try: 146 | pkg_resources.require("setuptools>=" + version) 147 | return 148 | except pkg_resources.VersionConflict: 149 | e = sys.exc_info()[1] 150 | if was_imported: 151 | sys.stderr.write( 152 | "The required version of setuptools (>=%s) is not available,\n" 153 | "and can't be installed while this script is running. Please\n" 154 | "install a more recent version first, using\n" 155 | "'easy_install -U setuptools'." 156 | "\n\n(Currently using %r)\n" % (version, e.args[0]) 157 | ) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules["pkg_resources"] # reload ok 161 | return _do_download(version, download_base, to_dir, download_delay) 162 | except pkg_resources.DistributionNotFound: 163 | return _do_download(version, download_base, to_dir, download_delay) 164 | 165 | 166 | def download_file_powershell(url, target): 167 | """ 168 | Download the file at url to target using Powershell (which will validate 169 | trust). Raise an exception if the command cannot complete. 170 | """ 171 | target = os.path.abspath(target) 172 | cmd = [ 173 | "powershell", 174 | "-Command", 175 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), 176 | ] 177 | subprocess.check_call(cmd) 178 | 179 | 180 | def has_powershell(): 181 | if platform.system() != "Windows": 182 | return False 183 | cmd = ["powershell", "-Command", "echo test"] 184 | devnull = open(os.path.devnull, "wb") 185 | try: 186 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 187 | except: # noqa: E722 188 | return False 189 | finally: 190 | devnull.close() 191 | return True 192 | 193 | 194 | download_file_powershell.viable = has_powershell 195 | 196 | 197 | def download_file_curl(url, target): 198 | cmd = ["curl", url, "--silent", "--output", target] 199 | subprocess.check_call(cmd) 200 | 201 | 202 | def has_curl(): 203 | cmd = ["curl", "--version"] 204 | devnull = open(os.path.devnull, "wb") 205 | try: 206 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 207 | except: # noqa: E722 208 | return False 209 | finally: 210 | devnull.close() 211 | return True 212 | 213 | 214 | download_file_curl.viable = has_curl 215 | 216 | 217 | def download_file_wget(url, target): 218 | cmd = ["wget", url, "--quiet", "--output-document", target] 219 | subprocess.check_call(cmd) 220 | 221 | 222 | def has_wget(): 223 | cmd = ["wget", "--version"] 224 | devnull = open(os.path.devnull, "wb") 225 | try: 226 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 227 | except: # noqa: E722 228 | return False 229 | finally: 230 | devnull.close() 231 | return True 232 | 233 | 234 | download_file_wget.viable = has_wget 235 | 236 | 237 | def download_file_insecure(url, target): 238 | """ 239 | Use Python to download the file, even though it cannot authenticate the 240 | connection. 241 | """ 242 | try: 243 | from urllib.request import urlopen 244 | except ImportError: 245 | from urllib.request import urlopen 246 | src = dst = None 247 | try: 248 | src = urlopen(url) 249 | # Read/write all in one block, so we don't create a corrupt file 250 | # if the download is interrupted. 251 | data = src.read() 252 | dst = open(target, "wb") 253 | dst.write(data) 254 | finally: 255 | if src: 256 | src.close() 257 | if dst: 258 | dst.close() 259 | 260 | 261 | download_file_insecure.viable = lambda: True 262 | 263 | 264 | def get_best_downloader(): 265 | downloaders = [ 266 | download_file_powershell, 267 | download_file_curl, 268 | download_file_wget, 269 | download_file_insecure, 270 | ] 271 | 272 | for dl in downloaders: 273 | if dl.viable(): 274 | return dl 275 | 276 | 277 | def download_setuptools( 278 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15 279 | ): 280 | """Download setuptools from a specified location and return its filename 281 | 282 | `version` should be a valid setuptools version number that is available 283 | as an egg for download under the `download_base` URL (which should end 284 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 285 | `delay` is the number of seconds to pause before an actual download 286 | attempt. 287 | """ 288 | # making sure we use the absolute path 289 | to_dir = os.path.abspath(to_dir) 290 | tgz_name = "setuptools-%s.tar.gz" % version 291 | url = download_base + tgz_name 292 | saveto = os.path.join(to_dir, tgz_name) 293 | if not os.path.exists(saveto): # Avoid repeated downloads 294 | log.warn("Downloading %s", url) 295 | downloader = get_best_downloader() 296 | downloader(url, saveto) 297 | return os.path.realpath(saveto) 298 | 299 | 300 | def _extractall(self, path=".", members=None): 301 | """Extract all members from the archive to the current working 302 | directory and set owner, modification time and permissions on 303 | directories afterwards. `path' specifies a different directory 304 | to extract to. `members' is optional and must be a subset of the 305 | list returned by getmembers(). 306 | """ 307 | import copy 308 | import operator 309 | from tarfile import ExtractError 310 | 311 | directories = [] 312 | 313 | if members is None: 314 | members = self 315 | 316 | for tarinfo in members: 317 | if tarinfo.isdir(): 318 | # Extract directories with a safe mode. 319 | directories.append(tarinfo) 320 | tarinfo = copy.copy(tarinfo) 321 | tarinfo.mode = 448 # decimal for oct 0700 322 | self.extract(tarinfo, path) 323 | 324 | # Reverse sort directories. 325 | if sys.version_info < (2, 4): 326 | 327 | def sorter(dir1, dir2): 328 | return cmp(dir1.name, dir2.name) # noqa: F821 329 | 330 | directories.sort(sorter) 331 | directories.reverse() 332 | else: 333 | directories.sort(key=operator.attrgetter("name"), reverse=True) 334 | 335 | # Set correct owner, mtime and filemode on directories. 336 | for tarinfo in directories: 337 | dirpath = os.path.join(path, tarinfo.name) 338 | try: 339 | self.chown(tarinfo, dirpath) 340 | self.utime(tarinfo, dirpath) 341 | self.chmod(tarinfo, dirpath) 342 | except ExtractError: 343 | e = sys.exc_info()[1] 344 | if self.errorlevel > 1: 345 | raise 346 | else: 347 | self._dbg(1, "tarfile: %s" % e) 348 | 349 | 350 | def _build_install_args(options): 351 | """ 352 | Build the arguments to 'python setup.py install' on the setuptools package 353 | """ 354 | install_args = [] 355 | if options.user_install: 356 | if sys.version_info < (2, 6): 357 | log.warn("--user requires Python 2.6 or later") 358 | raise SystemExit(1) 359 | install_args.append("--user") 360 | return install_args 361 | 362 | 363 | def _parse_args(): 364 | """ 365 | Parse the command line for options 366 | """ 367 | parser = optparse.OptionParser() 368 | parser.add_option( 369 | "--user", 370 | dest="user_install", 371 | action="store_true", 372 | default=False, 373 | help="install in user site package (requires Python 2.6 or later)", 374 | ) 375 | parser.add_option( 376 | "--download-base", 377 | dest="download_base", 378 | metavar="URL", 379 | default=DEFAULT_URL, 380 | help="alternative URL from where to download the setuptools package", 381 | ) 382 | options, args = parser.parse_args() 383 | # positional arguments are ignored 384 | return options 385 | 386 | 387 | def main(version=DEFAULT_VERSION): 388 | """Install or upgrade setuptools and EasyInstall""" 389 | options = _parse_args() 390 | tarball = download_setuptools(download_base=options.download_base) 391 | return _install(tarball, _build_install_args(options)) 392 | 393 | 394 | if __name__ == "__main__": 395 | sys.exit(main()) 396 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from setuptools import find_packages, setup 3 | 4 | 5 | def readme(): 6 | with open("README.md") as f: 7 | return f.read() 8 | 9 | 10 | setup( 11 | name="dexcom_reader", 12 | version="0.2.0", # http://semver.org/ 13 | description="Audit, and inspect data from Dexcom G4, G5, and G6 receivers.", 14 | long_description=readme(), 15 | author="Will Nowak", 16 | # I'm just maintaining the package, @compbrain authored it. 17 | author_email="compbrain+dexcom_reader@gmail.com", 18 | maintainer="Ben West", 19 | maintainer_email="bewest+dexcom_reader@gmail.com", 20 | url="https://github.com/openaps/dexcom_reader", 21 | packages=find_packages(), 22 | install_requires=["pyserial"], 23 | classifiers=[ 24 | "Development Status :: 3 - Alpha", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Science/Research", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.5", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Topic :: Scientific/Engineering", 31 | "Topic :: Software Development :: Libraries", 32 | ], 33 | package_data={"dexcom_reader": ["etc/udev/rules.d/*"]}, 34 | data_files=[ 35 | # installing to system locations breaks things for virtualenv based 36 | # environments. 37 | # ('/etc/udev/rules.d', ['dexcom_reader/etc/udev/rules.d/80-dexcom.rules'] ), 38 | ], 39 | zip_safe=False, 40 | include_package_data=True, 41 | ) 42 | 43 | ##### 44 | # EOF 45 | --------------------------------------------------------------------------------