diff options
author | Will Nowak <compbrain@gmail.com> | 2013-03-13 23:44:05 -0700 |
---|---|---|
committer | Will Nowak <compbrain@gmail.com> | 2013-03-13 23:44:05 -0700 |
commit | 1c144a3e89255a54f908d9249316619744fb58c7 (patch) | |
tree | 5f6f8c33b0e248b781b09f422d0f3356eac0fda3 | |
parent | 59141c8d1f3aeddc7df5b99576c73d48a9e16be6 (diff) |
More complete data reader
Should implement all the functionality one needs to avoid using the provided
software that ships with the device.
-rw-r--r-- | LICENSE | 5 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | constants.py | 94 | ||||
-rw-r--r-- | crc16.py | 37 | ||||
-rw-r--r-- | database_records.py | 189 | ||||
-rw-r--r-- | packetwriter.py | 49 | ||||
-rw-r--r-- | readdata.py | 306 | ||||
-rw-r--r-- | util.py | 82 |
8 files changed, 683 insertions, 97 deletions
@@ -0,0 +1,5 @@ | |||
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. | ||
@@ -1,2 +1,18 @@ | |||
1 | dexcom_reader | 1 | dexcom_reader |
2 | ============= \ No newline at end of file | 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 | ||
diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..2f60020 --- /dev/null +++ b/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/crc16.py b/crc16.py new file mode 100644 index 0000000..0a55332 --- /dev/null +++ b/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/database_records.py b/database_records.py new file mode 100644 index 0000000..bde0c67 --- /dev/null +++ b/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/packetwriter.py b/packetwriter.py new file mode 100644 index 0000000..6e03342 --- /dev/null +++ b/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/readdata.py b/readdata.py index 7fe86c2..c743b99 100644 --- a/readdata.py +++ b/readdata.py | |||
@@ -1,30 +1,60 @@ | |||
1 | """Dexcom G4 Platinum data reader. | 1 | import crc16 |
2 | 2 | import constants | |
3 | Copyright 2013 | 3 | import database_records |
4 | 4 | import datetime | |
5 | Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | you may not use this file except in compliance with the License. | ||
7 | You may obtain a copy of the License at | ||
8 | |||
9 | http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | |||
11 | Unless required by applicable law or agreed to in writing, software | ||
12 | distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | See the License for the specific language governing permissions and | ||
15 | limitations under the License. | ||
16 | """ | ||
17 | |||
18 | from curses import ascii as asciicc | ||
19 | import re | ||
20 | import serial | 5 | import serial |
21 | import sys | 6 | import sys |
22 | import time | 7 | import time |
8 | import packetwriter | ||
9 | import struct | ||
10 | import re | ||
11 | import util | ||
23 | import xml.etree.ElementTree as ET | 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 | ||
24 | 29 | ||
25 | 30 | ||
26 | class Dexcom(object): | 31 | class Dexcom(object): |
27 | BASE_PREFIX = "\x01\x06\x00" | 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'))) | ||
28 | 58 | ||
29 | def __init__(self, port): | 59 | def __init__(self, port): |
30 | self._port_name = port | 60 | self._port_name = port |
@@ -44,44 +74,110 @@ class Dexcom(object): | |||
44 | self.Connect() | 74 | self.Connect() |
45 | return self._port | 75 | return self._port |
46 | 76 | ||
47 | def Waiting(self, timeout=None): | ||
48 | start_time = time.time() | ||
49 | while True: | ||
50 | if (timeout is not None) and (time.time() - start_time) > timeout: | ||
51 | return 0 | ||
52 | n = self.port.inWaiting() | ||
53 | if n: | ||
54 | return n | ||
55 | time.sleep(0.1) | ||
56 | |||
57 | def write(self, *args, **kwargs): | 77 | def write(self, *args, **kwargs): |
58 | return self.port.write(*args, **kwargs) | 78 | return self.port.write(*args, **kwargs) |
59 | 79 | ||
60 | def read(self, *args, **kwargs): | 80 | def read(self, *args, **kwargs): |
61 | return self.port.read(*args, **kwargs) | 81 | return self.port.read(*args, **kwargs) |
62 | 82 | ||
63 | def readwaiting(self): | 83 | def readpacket(self, timeout=None): |
64 | waiting = self.Waiting() | 84 | total_read = 4 |
65 | return self.read(waiting) | 85 | intial_read = self.read(total_read) |
66 | 86 | all_data = initial_read | |
67 | def Handshake(self): | 87 | if ord(initial_read[0]) == 1: |
68 | self.write("%s\x0a\x5e\x65" % self.BASE_PREFIX) | 88 | command = initial_read[3] |
69 | if self.readwaiting() == "%s\x01\x35\xd4" % self.BASE_PREFIX: | 89 | data_number = struct.unpack('<H', initial_read[1:3])[0] |
70 | return True | 90 | if data_number > 6: |
71 | 91 | toread = abs(data_number-6) | |
72 | def IdentifyDevice(self): | 92 | second_read = self.read(toread) |
73 | iprefix = "\x01\x03\x01\x01" | 93 | all_data += second_read |
74 | self.write("%s\x0b\x7f\x75" % self.BASE_PREFIX) | 94 | total_read += toread |
75 | i = self.readwaiting() | 95 | out = second_read |
76 | # Strip prefix | 96 | else: |
77 | i = i[len(iprefix):] | 97 | out = '' |
78 | # Strip tail | 98 | suffix = self.read(2) |
79 | ta = i[-2:] | 99 | sent_crc = struct.unpack('<H', suffix)[0] |
80 | assert ta == "\xd8\xd4" | 100 | local_crc = crc16.crc16(all_data, 0, total_read) |
81 | i = i[:-2] | 101 | if sent_crc != local_crc |
82 | e = ET.fromstring(i) | 102 | raise constants.CrcError("readpacket Failed CRC check") |
83 | print 'Found %s' % e.get('ProductName') | 103 | num1 = total_read + 2 |
84 | return e | 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) | ||
85 | 181 | ||
86 | def flush(self): | 182 | def flush(self): |
87 | self.port.flush() | 183 | self.port.flush() |
@@ -90,56 +186,74 @@ class Dexcom(object): | |||
90 | self.port.flushInput() | 186 | self.port.flushInput() |
91 | self.port.flushOutput() | 187 | self.port.flushOutput() |
92 | 188 | ||
93 | def CleanData(self, i): | 189 | def GetFirmwareHeader(self): |
94 | i = ''.join(c for c in i if ord(c) >= 32) | 190 | i = self.GenericReadCommand(constants.READ_FIRMWARE_HEADER) |
95 | return i | 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) | ||
96 | 196 | ||
97 | def GetManufacturingParameters(self): | 197 | def DataPartitions(self): |
98 | #self.port.write("\x01\x07\x00\x10\x00\x0f\xf8") | 198 | i = self.GenericReadCommand(constants.READ_DATABASE_PARTITION_INFO) |
99 | #self.readwaiting() | 199 | return ET.fromstring(i.data) |
100 | #self.clear() | ||
101 | self.write("\x01\x0c\x00\x11\x00\x00\x00\x00\x00\x01\x6e\x45") | ||
102 | 200 | ||
103 | i = self.CleanData(self.readwaiting()) | 201 | def ReadDatabasePageRange(self, record_type): |
104 | i = i[8:-4] | 202 | record_type_index = constants.RECORD_TYPES.index(record_type) |
105 | e = ET.fromstring(i) | 203 | self.WriteCommand(constants.READ_DATABASE_PAGE_RANGE, |
106 | print 'Found %s (S/N: %s)' % (e.get('HardwarePartNumber'), | 204 | chr(record_type_index)) |
107 | e.get('SerialNumber')) | 205 | packet = self.readpacket() |
108 | return e | 206 | return struct.unpack('II', packet.data) |
109 | 207 | ||
110 | def GetFirmwareHeader(self): | 208 | def ReadDatabasePage(self, record_type, page): |
111 | self.write("\x01\x06\x00\x0b\x7f\x75") | 209 | record_type_index = constants.RECORD_TYPES.index(record_type) |
112 | i = self.CleanData(self.readwaiting())[:-2] | 210 | self.WriteCommand(constants.READ_DATABASE_PAGES, |
113 | e = ET.fromstring(i) | 211 | (chr(record_type_index), struct.pack('I', page), chr(1))) |
114 | return e | 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:] | ||
115 | 224 | ||
116 | def Ping(self): | 225 | return self.ParsePage(header, packet_data) |
117 | self.write("\x01\x07\x00\x10\x02\x4d\xd8") | ||
118 | self.readwaiting() | ||
119 | self.write("\x01\x07\x00\x10\x02\x4d\xd8") | ||
120 | self.readwaiting() | ||
121 | self.flush() | ||
122 | self.clear() | ||
123 | 226 | ||
124 | def GetPcParams(self): | 227 | def GenericRecordYielder(self, header, data, record_type): |
125 | self.write("\x01\x0c\x00\x11\x02\x00\x00\x00\x00\x01\x2e\xce") | 228 | for x in range(header[1]): |
126 | i =self.CleanData(self.readwaiting())[8:-3] | 229 | yield record_type.Create(data, x) |
127 | e = ET.fromstring(i) | ||
128 | return e | ||
129 | 230 | ||
130 | def DataPartitions(self): | 231 | def ParsePage(self, header, data): |
131 | self.write("\x01\x06\x00\x36\x81\x92") | 232 | record_type = constants.RECORD_TYPES[ord(header[2])] |
132 | print self.readwaiting() | 233 | generic_parser_map = { |
133 | self.write("\x01\x06\x00\x0f\xfb\x35") | 234 | 'USER_EVENT_DATA': database_records.EventRecord, |
134 | print self.readwaiting() | 235 | 'METER_DATA': database_records.MeterRecord, |
135 | print self.readwaiting() | 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) | ||
136 | 248 | ||
137 | d = Dexcom(sys.argv[1]) | 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 | ||
138 | 256 | ||
139 | if d.Handshake(): | ||
140 | print "Connected successfully!" | ||
141 | 257 | ||
142 | print d.IdentifyDevice() | 258 | if __name__ == '__main__': |
143 | print d.GetManufacturingParameters() | 259 | Dexcom.LocateAndDownload() |
144 | print d.GetFirmwareHeader() | ||
145 | print d.GetPcParams() | ||
@@ -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()) | ||