diff options
author | Ben West <bewest@gmail.com> | 2014-05-24 16:42:13 -0700 |
---|---|---|
committer | Ben West <bewest@gmail.com> | 2014-05-24 16:42:13 -0700 |
commit | 5d92368e5b10fb2d01ae25a2730e15cd05a84da6 (patch) | |
tree | 6f5264f423e8e5a9ddfd44f3222bc8ad215b6e2f /dexcom_reader | |
parent | 6b5ce2584b0fe2da991540a33c71b1f95cd8878a (diff) |
prep for installable module
Diffstat (limited to 'dexcom_reader')
-rw-r--r-- | dexcom_reader/__init__.py | 0 | ||||
-rw-r--r-- | dexcom_reader/constants.py | 94 | ||||
-rw-r--r-- | dexcom_reader/crc16.py | 37 | ||||
-rw-r--r-- | dexcom_reader/database_records.py | 189 | ||||
-rw-r--r-- | dexcom_reader/packetwriter.py | 49 | ||||
-rw-r--r-- | dexcom_reader/readdata.py | 259 | ||||
-rw-r--r-- | dexcom_reader/util.py | 82 |
7 files changed, 710 insertions, 0 deletions
diff --git a/dexcom_reader/__init__.py b/dexcom_reader/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/dexcom_reader/__init__.py | |||
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 @@ | |||
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_G4_USB_VENDOR = 0x22a3 | ||
13 | DEXCOM_G4_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 | MAX_COMMAND = 59 | ||
59 | MAX_POSSIBLE_COMMAND = 255 | ||
60 | |||
61 | EGV_VALUE_MASK = 1023 | ||
62 | EGV_DISPLAY_ONLY_MASK = 32768 | ||
63 | EGV_TREND_ARROW_MASK = 15 | ||
64 | |||
65 | BATTERY_STATES = [None, 'CHARGING', 'NOT_CHARGING', 'NTC_FAULT', 'BAD_BATTERY'] | ||
66 | |||
67 | RECORD_TYPES = [ | ||
68 | 'MANUFACTURING_DATA', 'FIRMWARE_PARAMETER_DATA', 'PC_SOFTWARE_PARAMETER', | ||
69 | 'SENSOR_DATA', 'EGV_DATA', 'CAL_SET', 'DEVIATION', 'INSERTION_TIME', | ||
70 | 'RECEIVER_LOG_DATA', 'RECEIVER_ERROR_DATA', 'METER_DATA', 'USER_EVENT_DATA', | ||
71 | 'USER_SETTING_DATA', 'MAX_VALUE', | ||
72 | ] | ||
73 | |||
74 | TREND_ARROW_VALUES = [None, 'DOUBLE_UP', 'SINGLE_UP', '45_UP', 'FLAT', | ||
75 | '45_DOWN', 'SINGLE_DOWN', 'DOUBLE_DOWN', 'NOT_COMPUTABLE', | ||
76 | 'OUT_OF_RANGE'] | ||
77 | |||
78 | SPECIAL_GLUCOSE_VALUES = {0: None, | ||
79 | 1: 'SENSOR_NOT_ACTIVE', | ||
80 | 2: 'MINIMAL_DEVIATION', | ||
81 | 3: 'NO_ANTENNA', | ||
82 | 5: 'SENSOR_NOT_CALIBRATED', | ||
83 | 6: 'COUNTS_DEVIATION', | ||
84 | 9: 'ABSOLUTE_DEVIATION', | ||
85 | 10: 'POWER_DEVIATION', | ||
86 | 12: 'BAD_RF'} | ||
87 | |||
88 | |||
89 | LANGUAGES = { | ||
90 | 0: None, | ||
91 | 1033: 'ENGLISH', | ||
92 | } | ||
93 | |||
94 | |||
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 @@ | |||
1 | TABLE = [ | ||
2 | 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, | ||
3 | 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, | ||
4 | 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, | ||
5 | 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, | ||
6 | 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, | ||
7 | 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, | ||
8 | 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, | ||
9 | 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, | ||
10 | 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, | ||
11 | 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, | ||
12 | 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, | ||
13 | 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, | ||
14 | 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, | ||
15 | 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, | ||
16 | 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, | ||
17 | 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, | ||
18 | 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, | ||
19 | 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, | ||
20 | 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, | ||
21 | 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, | ||
22 | 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, | ||
23 | 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, | ||
24 | 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, | ||
25 | 16050, 3793, 7920 | ||
26 | ] | ||
27 | |||
28 | |||
29 | def crc16(buf, start=None, end=None): | ||
30 | if start is None: | ||
31 | start = 0 | ||
32 | if end is None: | ||
33 | end = len(buf) | ||
34 | num = 0 | ||
35 | for i in range(start, end): | ||
36 | num = ((num<<8)&0xff00) ^ TABLE[((num>>8)&0xff)^ord(buf[i])] | ||
37 | 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 @@ | |||
1 | import crc16 | ||
2 | import constants | ||
3 | import struct | ||
4 | import util | ||
5 | |||
6 | |||
7 | class BaseDatabaseRecord(object): | ||
8 | FORMAT = None | ||
9 | |||
10 | @classmethod | ||
11 | def _CheckFormat(cls): | ||
12 | if cls.FORMAT is None or not cls.FORMAT: | ||
13 | raise NotImplementedError("Subclasses of %s need to define FORMAT" | ||
14 | % cls.__name__) | ||
15 | |||
16 | @classmethod | ||
17 | def _ClassFormat(cls): | ||
18 | cls._CheckFormat() | ||
19 | return struct.Struct(cls.FORMAT) | ||
20 | |||
21 | @classmethod | ||
22 | def _ClassSize(cls): | ||
23 | return cls._ClassFormat().size | ||
24 | |||
25 | @property | ||
26 | def FMT(self): | ||
27 | self._CheckFormat() | ||
28 | return _ClassFormat() | ||
29 | |||
30 | @property | ||
31 | def SIZE(self): | ||
32 | return self._ClassSize() | ||
33 | |||
34 | @property | ||
35 | def crc(self): | ||
36 | return self.data[-1] | ||
37 | |||
38 | def __init__(self, data, raw_data): | ||
39 | self.raw_data = raw_data | ||
40 | self.data = data | ||
41 | self.check_crc() | ||
42 | |||
43 | def check_crc(self): | ||
44 | local_crc = self.calculate_crc() | ||
45 | if local_crc != self.crc: | ||
46 | raise constants.CrcError('Could not parse %s' % self.__class__.__name__) | ||
47 | |||
48 | def dump(self): | ||
49 | return ''.join('\\x%02x' % ord(c) for c in self.raw_data) | ||
50 | |||
51 | def calculate_crc(self): | ||
52 | return crc16.crc16(self.raw_data[:-2]) | ||
53 | |||
54 | @classmethod | ||
55 | def Create(cls, data, record_counter): | ||
56 | offset = record_counter * cls._ClassSize() | ||
57 | raw_data = data[offset:offset + cls._ClassSize()] | ||
58 | unpacked_data = cls._ClassFormat().unpack(raw_data) | ||
59 | return cls(unpacked_data, raw_data) | ||
60 | |||
61 | |||
62 | class GenericTimestampedRecord(BaseDatabaseRecord): | ||
63 | @property | ||
64 | def system_time(self): | ||
65 | return util.ReceiverTimeToTime(self.data[0]) | ||
66 | |||
67 | @property | ||
68 | def display_time(self): | ||
69 | return util.ReceiverTimeToTime(self.data[1]) | ||
70 | |||
71 | |||
72 | class GenericXMLRecord(GenericTimestampedRecord): | ||
73 | FORMAT = '<II490sH' | ||
74 | |||
75 | @property | ||
76 | def xmldata(self): | ||
77 | data = self.data[2].replace("\x00", "") | ||
78 | return data | ||
79 | |||
80 | |||
81 | class InsertionRecord(GenericTimestampedRecord): | ||
82 | FORMAT = '<3IcH' | ||
83 | |||
84 | @property | ||
85 | def insertion_time(self): | ||
86 | return util.ReceiverTimeToTime(self.data[2]) | ||
87 | |||
88 | @property | ||
89 | def session_state(self): | ||
90 | states = [None, 'REMOVED', 'EXPIRED', 'RESIDUAL_DEVIATION', | ||
91 | 'COUNTS_DEVIATION', 'SECOND_SESSION', 'OFF_TIME_LOSS', | ||
92 | 'STARTED', 'BAD_TRANSMITTER', 'MANUFACTURING_MODE'] | ||
93 | return states[ord(self.data[3])] | ||
94 | |||
95 | def __repr__(self): | ||
96 | return '%s: state=%s' % (self.display_time, self.session_state) | ||
97 | |||
98 | |||
99 | class MeterRecord(GenericTimestampedRecord): | ||
100 | FORMAT = '<2IHIH' | ||
101 | |||
102 | @property | ||
103 | def meter_glucose(self): | ||
104 | return self.data[2] | ||
105 | |||
106 | @property | ||
107 | def meter_time(self): | ||
108 | return util.ReceiverTimeToTime(self.data[3]) | ||
109 | |||
110 | def __repr__(self): | ||
111 | return '%s: Meter BG:%s' % (self.display_time, self.meter_glucose) | ||
112 | |||
113 | |||
114 | class EventRecord(GenericTimestampedRecord): | ||
115 | # sys_time,display_time,glucose,meter_time,crc | ||
116 | FORMAT = '<2I2c2IH' | ||
117 | |||
118 | @property | ||
119 | def event_type(self): | ||
120 | event_types = [None, 'CARBS', 'INSULIN', 'HEALTH', 'EXCERCISE', | ||
121 | 'MAX_VALUE'] | ||
122 | return event_types[ord(self.data[2])] | ||
123 | |||
124 | @property | ||
125 | def event_sub_type(self): | ||
126 | subtypes = {'HEALTH': [None, 'ILLNESS', 'STRESS', 'HIGH_SYMPTOMS', | ||
127 | 'LOW_SYMTOMS', 'CYCLE', 'ALCOHOL'], | ||
128 | 'EXCERCISE': [None, 'LIGHT', 'MEDIUM', 'HEAVY', | ||
129 | 'MAX_VALUE']} | ||
130 | if self.event_type in subtypes: | ||
131 | return subtypes[self.event_type][ord(self.data[3])] | ||
132 | |||
133 | @property | ||
134 | def display_time(self): | ||
135 | return util.ReceiverTimeToTime(self.data[4]) | ||
136 | |||
137 | @property | ||
138 | def event_value(self): | ||
139 | value = self.data[5] | ||
140 | if self.event_type == 'INSULIN': | ||
141 | value = value / 100.0 | ||
142 | return value | ||
143 | |||
144 | def __repr__(self): | ||
145 | return '%s: event_type=%s sub_type=%s value=%s' % (self.display_time, self.event_type, | ||
146 | self.event_sub_type, self.event_value) | ||
147 | |||
148 | |||
149 | class EGVRecord(GenericTimestampedRecord): | ||
150 | # uint, uint, ushort, byte, ushort | ||
151 | # (system_seconds, display_seconds, glucose, trend_arrow, crc) | ||
152 | FORMAT = '<2IHcH' | ||
153 | |||
154 | @property | ||
155 | def full_glucose(self): | ||
156 | return self.data[2] | ||
157 | |||
158 | @property | ||
159 | def full_trend(self): | ||
160 | return self.data[3] | ||
161 | |||
162 | @property | ||
163 | def display_only(self): | ||
164 | return bool(self.full_glucose & constants.EGV_DISPLAY_ONLY_MASK) | ||
165 | |||
166 | @property | ||
167 | def glucose(self): | ||
168 | return self.full_glucose & constants.EGV_VALUE_MASK | ||
169 | |||
170 | @property | ||
171 | def glucose_special_meaning(self): | ||
172 | if self.glucose in constants.SPECIAL_GLUCOSE_VALUES: | ||
173 | return constants.SPECIAL_GLUCOSE_VALUES[self.glucose] | ||
174 | |||
175 | @property | ||
176 | def is_special(self): | ||
177 | return self.glucose_special_meaning is not None | ||
178 | |||
179 | @property | ||
180 | def trend_arrow(self): | ||
181 | arrow_value = ord(self.full_trend) & constants.EGV_TREND_ARROW_MASK | ||
182 | return constants.TREND_ARROW_VALUES[arrow_value] | ||
183 | |||
184 | def __repr__(self): | ||
185 | if self.is_special: | ||
186 | return '%s: %s' % (self.display_time, self.glucose_special_meaning) | ||
187 | else: | ||
188 | return '%s: CGM BG:%s (%s) DO:%s' % (self.display_time, self.glucose, | ||
189 | self.trend_arrow, self.display_only) | ||
diff --git a/dexcom_reader/packetwriter.py b/dexcom_reader/packetwriter.py new file mode 100644 index 0000000..6e03342 --- /dev/null +++ b/dexcom_reader/packetwriter.py | |||
@@ -0,0 +1,49 @@ | |||
1 | import crc16 | ||
2 | import struct | ||
3 | |||
4 | class PacketWriter(object): | ||
5 | MAX_PAYLOAD = 1584 | ||
6 | MIN_LEN = 6 | ||
7 | MAX_LEN = 1590 | ||
8 | SOF = 0x01 | ||
9 | OFFSET_SOF = 0 | ||
10 | OFFSET_LENGTH = 1 | ||
11 | OFFSET_CMD = 3 | ||
12 | OFFSET_PAYLOAD = 4 | ||
13 | |||
14 | def __init__(self): | ||
15 | self._packet = None | ||
16 | |||
17 | def Clear(self): | ||
18 | self._packet = None | ||
19 | |||
20 | def NewSOF(self, v): | ||
21 | self._packet[0] = chr(v) | ||
22 | |||
23 | def PacketString(self): | ||
24 | return ''.join(self._packet) | ||
25 | |||
26 | def AppendCrc(self): | ||
27 | self.SetLength() | ||
28 | ps = self.PacketString() | ||
29 | crc = crc16.crc16(ps, 0, len(ps)) | ||
30 | for x in struct.pack('H', crc): | ||
31 | self._packet.append(x) | ||
32 | |||
33 | def SetLength(self): | ||
34 | self._packet[1] = chr(len(self._packet) + 2) | ||
35 | |||
36 | def _Add(self, x): | ||
37 | try: | ||
38 | len(x) | ||
39 | for y in x: | ||
40 | self._Add(y) | ||
41 | except: | ||
42 | self._packet.append(x) | ||
43 | |||
44 | def ComposePacket(self, command, payload=None): | ||
45 | assert self._packet is None | ||
46 | self._packet = ["\x01", None, "\x00", chr(command)] | ||
47 | if payload: | ||
48 | self._Add(payload) | ||
49 | self.AppendCrc() | ||
diff --git a/dexcom_reader/readdata.py b/dexcom_reader/readdata.py new file mode 100644 index 0000000..f398407 --- /dev/null +++ b/dexcom_reader/readdata.py | |||
@@ -0,0 +1,259 @@ | |||
1 | import crc16 | ||
2 | import constants | ||
3 | import database_records | ||
4 | import datetime | ||
5 | import serial | ||
6 | import sys | ||
7 | import time | ||
8 | import packetwriter | ||
9 | import struct | ||
10 | import re | ||
11 | import util | ||
12 | import xml.etree.ElementTree as ET | ||
13 | import numpy | ||
14 | import platform | ||
15 | |||
16 | |||
17 | class ReadPacket(object): | ||
18 | def __init__(self, command, data): | ||
19 | self._command = command | ||
20 | self._data = data | ||
21 | |||
22 | @property | ||
23 | def command(self): | ||
24 | return self._command | ||
25 | |||
26 | @property | ||
27 | def data(self): | ||
28 | return self._data | ||
29 | |||
30 | |||
31 | class Dexcom(object): | ||
32 | @staticmethod | ||
33 | def FindDevice(): | ||
34 | return util.find_usbserial(constants.DEXCOM_G4_USB_VENDOR, | ||
35 | constants.DEXCOM_G4_USB_PRODUCT) | ||
36 | |||
37 | @classmethod | ||
38 | def LocateAndDownload(cls): | ||
39 | device = cls.FindDevice() | ||
40 | if not device: | ||
41 | sys.stderr.write('Could not find Dexcom G4 Receiver!\n') | ||
42 | sys.exit(1) | ||
43 | else: | ||
44 | dex = cls(device) | ||
45 | print ('Found %s S/N: %s' | ||
46 | % (dex.GetFirmwareHeader().get('ProductName'), | ||
47 | dex.ReadManufacturingData().get('SerialNumber'))) | ||
48 | print 'Transmitter paired: %s' % dex.ReadTransmitterId() | ||
49 | print 'Battery Status: %s (%d%%)' % (dex.ReadBatteryState(), | ||
50 | dex.ReadBatteryLevel()) | ||
51 | print 'Record count:' | ||
52 | print '- Meter records: %d' % (len(dex.ReadRecords('METER_DATA'))) | ||
53 | print '- CGM records: %d' % (len(dex.ReadRecords('EGV_DATA'))) | ||
54 | print ('- CGM commitable records: %d' | ||
55 | % (len([not x.display_only for x in dex.ReadRecords('EGV_DATA')]))) | ||
56 | print '- Event records: %d' % (len(dex.ReadRecords('USER_EVENT_DATA'))) | ||
57 | print '- Insertion records: %d' % (len(dex.ReadRecords('INSERTION_TIME'))) | ||
58 | |||
59 | def __init__(self, port): | ||
60 | self._port_name = port | ||
61 | self._port = None | ||
62 | |||
63 | def Connect(self): | ||
64 | if self._port is None: | ||
65 | self._port = serial.Serial(port=self._port_name, baudrate=115200) | ||
66 | |||
67 | def Disconnect(self): | ||
68 | if self._port is not None: | ||
69 | self._port.close() | ||
70 | |||
71 | @property | ||
72 | def port(self): | ||
73 | if self._port is None: | ||
74 | self.Connect() | ||
75 | return self._port | ||
76 | |||
77 | def write(self, *args, **kwargs): | ||
78 | return self.port.write(*args, **kwargs) | ||
79 | |||
80 | def read(self, *args, **kwargs): | ||
81 | return self.port.read(*args, **kwargs) | ||
82 | |||
83 | def readpacket(self, timeout=None): | ||
84 | total_read = 4 | ||
85 | initial_read = self.read(total_read) | ||
86 | all_data = initial_read | ||
87 | if ord(initial_read[0]) == 1: | ||
88 | command = initial_read[3] | ||
89 | data_number = struct.unpack('<H', initial_read[1:3])[0] | ||
90 | if data_number > 6: | ||
91 | toread = abs(data_number-6) | ||
92 | second_read = self.read(toread) | ||
93 | all_data += second_read | ||
94 | total_read += toread | ||
95 | out = second_read | ||
96 | else: | ||
97 | out = '' | ||
98 | suffix = self.read(2) | ||
99 | sent_crc = struct.unpack('<H', suffix)[0] | ||
100 | local_crc = crc16.crc16(all_data, 0, total_read) | ||
101 | if sent_crc != local_crc: | ||
102 | raise constants.CrcError("readpacket Failed CRC check") | ||
103 | num1 = total_read + 2 | ||
104 | return ReadPacket(command, out) | ||
105 | else: | ||
106 | raise constants.Error('Error reading packet header!') | ||
107 | |||
108 | def Ping(self): | ||
109 | self.WriteCommand(constants.PING) | ||
110 | packet = self.readpacket() | ||
111 | return ord(packet.command) == constants.ACK | ||
112 | |||
113 | def WritePacket(self, packet): | ||
114 | if not packet: | ||
115 | raise constants.Error('Need a packet to send') | ||
116 | packetlen = len(packet) | ||
117 | if packetlen < 6 or packetlen > 1590: | ||
118 | raise constants.Error('Invalid packet length') | ||
119 | self.flush() | ||
120 | self.write(packet) | ||
121 | |||
122 | def WriteCommand(self, command_id, *args, **kwargs): | ||
123 | p = packetwriter.PacketWriter() | ||
124 | p.ComposePacket(command_id, *args, **kwargs) | ||
125 | self.WritePacket(p.PacketString()) | ||
126 | |||
127 | def GenericReadCommand(self, command_id): | ||
128 | self.WriteCommand(command_id) | ||
129 | return self.readpacket() | ||
130 | |||
131 | def ReadTransmitterId(self): | ||
132 | return self.GenericReadCommand(constants.READ_TRANSMITTER_ID).data | ||
133 | |||
134 | def ReadLanguage(self): | ||
135 | lang = self.GenericReadCommand(constants.READ_LANGUAGE).data | ||
136 | return constants.LANGUAGES[struct.unpack('H', lang)[0]] | ||
137 | |||
138 | def ReadBatteryLevel(self): | ||
139 | level = self.GenericReadCommand(constants.READ_BATTERY_LEVEL).data | ||
140 | return struct.unpack('I', level)[0] | ||
141 | |||
142 | def ReadBatteryState(self): | ||
143 | state = self.GenericReadCommand(constants.READ_BATTERY_STATE).data | ||
144 | return constants.BATTERY_STATES[ord(state)] | ||
145 | |||
146 | def ReadRTC(self): | ||
147 | rtc = self.GenericReadCommand(constants.READ_RTC).data | ||
148 | return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0]) | ||
149 | |||
150 | def ReadSystemTime(self): | ||
151 | rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME).data | ||
152 | return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0]) | ||
153 | |||
154 | def ReadSystemTimeOffset(self): | ||
155 | rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME_OFFSET).data | ||
156 | return datetime.timedelta(seconds=struct.unpack('i', rtc)[0]) | ||
157 | |||
158 | def ReadDisplayTimeOffset(self): | ||
159 | rtc = self.GenericReadCommand(constants.READ_DISPLAY_TIME_OFFSET).data | ||
160 | return datetime.timedelta(seconds=struct.unpack('i', rtc)[0]) | ||
161 | |||
162 | def ReadDisplayTime(self): | ||
163 | return self.ReadSystemTime() + self.ReadDisplayTimeOffset() | ||
164 | |||
165 | def ReadGlucoseUnit(self): | ||
166 | UNIT_TYPE = (None, 'mg/dL', 'mmol/L') | ||
167 | gu = self.GenericReadCommand(constants.READ_GLUCOSE_UNIT).data | ||
168 | return UNIT_TYPE[ord(gu[0])] | ||
169 | |||
170 | def ReadClockMode(self): | ||
171 | CLOCK_MODE = (24, 12) | ||
172 | cm = self.GenericReadCommand(constants.READ_CLOCK_MODE).data | ||
173 | return CLOCK_MODE[ord(cm[0])] | ||
174 | |||
175 | def ReadDeviceMode(self): | ||
176 | return self.GenericReadCommand(constants.READ_DEVICE_MODE).data | ||
177 | |||
178 | def ReadManufacturingData(self): | ||
179 | data = self.ReadRecords('MANUFACTURING_DATA')[0].xmldata | ||
180 | return ET.fromstring(data) | ||
181 | |||
182 | def flush(self): | ||
183 | self.port.flush() | ||
184 | |||
185 | def clear(self): | ||
186 | self.port.flushInput() | ||
187 | self.port.flushOutput() | ||
188 | |||
189 | def GetFirmwareHeader(self): | ||
190 | i = self.GenericReadCommand(constants.READ_FIRMWARE_HEADER) | ||
191 | return ET.fromstring(i.data) | ||
192 | |||
193 | def GetFirmwareSettings(self): | ||
194 | i = self.GenericReadCommand(constants.READ_FIRMWARE_SETTINGS) | ||
195 | return ET.fromstring(i.data) | ||
196 | |||
197 | def DataPartitions(self): | ||
198 | i = self.GenericReadCommand(constants.READ_DATABASE_PARTITION_INFO) | ||
199 | return ET.fromstring(i.data) | ||
200 | |||
201 | def ReadDatabasePageRange(self, record_type): | ||
202 | record_type_index = constants.RECORD_TYPES.index(record_type) | ||
203 | self.WriteCommand(constants.READ_DATABASE_PAGE_RANGE, | ||
204 | chr(record_type_index)) | ||
205 | packet = self.readpacket() | ||
206 | return struct.unpack('II', packet.data) | ||
207 | |||
208 | def ReadDatabasePage(self, record_type, page): | ||
209 | record_type_index = constants.RECORD_TYPES.index(record_type) | ||
210 | self.WriteCommand(constants.READ_DATABASE_PAGES, | ||
211 | (chr(record_type_index), struct.pack('I', page), chr(1))) | ||
212 | packet = self.readpacket() | ||
213 | assert ord(packet.command) == 1 | ||
214 | # first index (uint), numrec (uint), record_type (byte), revision (byte), | ||
215 | # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc) | ||
216 | header_format = '<2I2c4IH' | ||
217 | header_data_len = struct.calcsize(header_format) | ||
218 | header = struct.unpack_from(header_format, packet.data) | ||
219 | header_crc = crc16.crc16(packet.data[:header_data_len-2]) | ||
220 | assert header_crc == header[-1] | ||
221 | assert ord(header[2]) == record_type_index | ||
222 | assert header[4] == page | ||
223 | packet_data = packet.data[header_data_len:] | ||
224 | |||
225 | return self.ParsePage(header, packet_data) | ||
226 | |||
227 | def GenericRecordYielder(self, header, data, record_type): | ||
228 | for x in range(header[1]): | ||
229 | yield record_type.Create(data, x) | ||
230 | |||
231 | def ParsePage(self, header, data): | ||
232 | record_type = constants.RECORD_TYPES[ord(header[2])] | ||
233 | generic_parser_map = { | ||
234 | 'USER_EVENT_DATA': database_records.EventRecord, | ||
235 | 'METER_DATA': database_records.MeterRecord, | ||
236 | 'INSERTION_TIME': database_records.InsertionRecord, | ||
237 | 'EGV_DATA': database_records.EGVRecord, | ||
238 | } | ||
239 | xml_parsed = ['PC_SOFTWARE_PARAMETER', 'MANUFACTURING_DATA'] | ||
240 | if record_type in generic_parser_map: | ||
241 | return self.GenericRecordYielder(header, data, | ||
242 | generic_parser_map[record_type]) | ||
243 | elif record_type in xml_parsed: | ||
244 | return [database_records.GenericXMLRecord.Create(data, 0)] | ||
245 | else: | ||
246 | raise NotImplementedError('Parsing of %s has not yet been implemented' | ||
247 | % record_type) | ||
248 | |||
249 | def ReadRecords(self, record_type): | ||
250 | records = [] | ||
251 | assert record_type in constants.RECORD_TYPES | ||
252 | page_range = self.ReadDatabasePageRange(record_type) | ||
253 | for x in range(page_range[0], page_range[1] or 1): | ||
254 | records.extend(self.ReadDatabasePage(record_type, x)) | ||
255 | return records | ||
256 | |||
257 | |||
258 | if __name__ == '__main__': | ||
259 | 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 @@ | |||
1 | import constants | ||
2 | import datetime | ||
3 | import os | ||
4 | import platform | ||
5 | import plistlib | ||
6 | import re | ||
7 | import subprocess | ||
8 | |||
9 | |||
10 | def ReceiverTimeToTime(rtime): | ||
11 | return constants.BASE_TIME + datetime.timedelta(seconds=rtime) | ||
12 | |||
13 | |||
14 | def linux_find_usbserial(vendor, product): | ||
15 | DEV_REGEX = re.compile('^tty(USB|ACM)[0-9]+$') | ||
16 | for usb_dev_root in os.listdir('/sys/bus/usb/devices'): | ||
17 | device_name = os.path.join('/sys/bus/usb/devices', usb_dev_root) | ||
18 | if not os.path.exists(os.path.join(device_name, 'idVendor')): | ||
19 | continue | ||
20 | idv = open(os.path.join(device_name, 'idVendor')).read().strip() | ||
21 | if idv != vendor: | ||
22 | continue | ||
23 | idp = open(os.path.join(device_name, 'idProduct')).read().strip() | ||
24 | if idp != product: | ||
25 | continue | ||
26 | for root, dirs, files in os.walk(device_name): | ||
27 | for option in dirs + files: | ||
28 | if DEV_REGEX.match(option): | ||
29 | return os.path.join('/dev', option) | ||
30 | |||
31 | |||
32 | def osx_find_usbserial(vendor, product): | ||
33 | def recur(v): | ||
34 | if hasattr(v, '__iter__') and 'idVendor' in v and 'idProduct' in v: | ||
35 | if v['idVendor'] == vendor and v['idProduct'] == product: | ||
36 | tmp = v | ||
37 | while True: | ||
38 | if 'IODialinDevice' not in tmp and 'IORegistryEntryChildren' in tmp: | ||
39 | tmp = tmp['IORegistryEntryChildren'] | ||
40 | elif 'IODialinDevice' in tmp: | ||
41 | return tmp['IODialinDevice'] | ||
42 | else: | ||
43 | break | ||
44 | |||
45 | if type(v) == list: | ||
46 | for x in v: | ||
47 | out = recur(x) | ||
48 | if out is not None: | ||
49 | return out | ||
50 | elif type(v) == dict or issubclass(type(v), dict): | ||
51 | for x in v.values(): | ||
52 | out = recur(x) | ||
53 | if out is not None: | ||
54 | return out | ||
55 | |||
56 | sp = subprocess.Popen(['/usr/sbin/ioreg', '-k', 'IODialinDevice', | ||
57 | '-r', '-t', '-l', '-a', '-x'], | ||
58 | stdout=subprocess.PIPE, | ||
59 | stdin=subprocess.PIPE, stderr=subprocess.PIPE) | ||
60 | stdout, _ = sp.communicate() | ||
61 | plist = plistlib.readPlistFromString(stdout) | ||
62 | return recur(plist) | ||
63 | |||
64 | |||
65 | def find_usbserial(vendor, product): | ||
66 | """Find the tty device for a given usbserial devices identifiers. | ||
67 | |||
68 | Args: | ||
69 | vendor: (int) something like 0x0000 | ||
70 | product: (int) something like 0x0000 | ||
71 | |||
72 | Returns: | ||
73 | String, like /dev/ttyACM0 or /dev/tty.usb... | ||
74 | """ | ||
75 | if platform.system() == 'Linux': | ||
76 | vendor, product = [('%04x' % (x)).strip() for x in (vendor, product)] | ||
77 | return linux_find_usbserial(vendor, product) | ||
78 | elif platform.system() == 'Darwin': | ||
79 | return osx_find_usbserial(vendor, product) | ||
80 | else: | ||
81 | raise NotImplementedError('Cannot find serial ports on %s' | ||
82 | % platform.system()) | ||