summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Nowak <compbrain@gmail.com>2013-03-13 23:44:05 -0700
committerWill Nowak <compbrain@gmail.com>2013-03-13 23:44:05 -0700
commit1c144a3e89255a54f908d9249316619744fb58c7 (patch)
tree5f6f8c33b0e248b781b09f422d0f3356eac0fda3
parent59141c8d1f3aeddc7df5b99576c73d48a9e16be6 (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--LICENSE5
-rw-r--r--README.md18
-rw-r--r--constants.py94
-rw-r--r--crc16.py37
-rw-r--r--database_records.py189
-rw-r--r--packetwriter.py49
-rw-r--r--readdata.py306
-rw-r--r--util.py82
8 files changed, 683 insertions, 97 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1f95d26
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,5 @@
1Permission 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
3The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
5THE 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.
diff --git a/README.md b/README.md
index 039680b..9cb44c3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,18 @@
1dexcom_reader 1dexcom_reader
2============= \ No newline at end of file 2=============
3
4This is a handful of scripts for dumping data from a Dexcom G4 Glucose Monitor
5connected to a computer with USB.
6
7Out of the box dumps data like:
8
9% python readdata.py
10Found Dexcom G4 Receiver S/N: SMXXXXXXXX
11Transmitter paired: 6XXXXX
12Battery 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 @@
1import datetime
2
3
4class Error(Exception):
5 """Base error for dexcom reader."""
6
7
8class CrcError(Error):
9 """Failed to CRC properly."""
10
11
12DEXCOM_G4_USB_VENDOR = 0x22a3
13DEXCOM_G4_USB_PRODUCT = 0x0047
14
15BASE_TIME = datetime.datetime(2009, 1, 1)
16
17NULL = 0
18ACK = 1
19NAK = 2
20INVALID_COMMAND = 3
21INVALID_PARAM = 4
22INCOMPLETE_PACKET_RECEIVED = 5
23RECEIVER_ERROR = 6
24INVALID_MODE = 7
25PING = 10
26READ_FIRMWARE_HEADER = 11
27READ_DATABASE_PARTITION_INFO = 15
28READ_DATABASE_PAGE_RANGE = 16
29READ_DATABASE_PAGES = 17
30READ_DATABASE_PAGE_HEADER = 18
31READ_TRANSMITTER_ID = 25
32WRITE_TRANSMITTER_ID = 26
33READ_LANGUAGE = 27
34WRITE_LANGUAGE = 28
35READ_DISPLAY_TIME_OFFSET = 29
36WRITE_DISPLAY_TIME_OFFSET = 30
37READ_RTC = 31
38RESET_RECEIVER = 32
39READ_BATTERY_LEVEL = 33
40READ_SYSTEM_TIME = 34
41READ_SYSTEM_TIME_OFFSET = 35
42WRITE_SYSTEM_TIME = 36
43READ_GLUCOSE_UNIT = 37
44WRITE_GLUCOSE_UNIT = 38
45READ_BLINDED_MODE = 39
46WRITE_BLINDED_MODE = 40
47READ_CLOCK_MODE = 41
48WRITE_CLOCK_MODE = 42
49READ_DEVICE_MODE = 43
50ERASE_DATABASE = 45
51SHUTDOWN_RECEIVER = 46
52WRITE_PC_PARAMETERS = 47
53READ_BATTERY_STATE = 48
54READ_HARDWARE_BOARD_ID = 49
55READ_FIRMWARE_SETTINGS = 54
56READ_ENABLE_SETUP_WIZARD_FLAG = 55
57READ_SETUP_WIZARD_STATE = 57
58MAX_COMMAND = 59
59MAX_POSSIBLE_COMMAND = 255
60
61EGV_VALUE_MASK = 1023
62EGV_DISPLAY_ONLY_MASK = 32768
63EGV_TREND_ARROW_MASK = 15
64
65BATTERY_STATES = [None, 'CHARGING', 'NOT_CHARGING', 'NTC_FAULT', 'BAD_BATTERY']
66
67RECORD_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
74TREND_ARROW_VALUES = [None, 'DOUBLE_UP', 'SINGLE_UP', '45_UP', 'FLAT',
75 '45_DOWN', 'SINGLE_DOWN', 'DOUBLE_DOWN', 'NOT_COMPUTABLE',
76 'OUT_OF_RANGE']
77
78SPECIAL_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
89LANGUAGES = {
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 @@
1TABLE = [
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
29def 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 @@
1import crc16
2import constants
3import struct
4import util
5
6
7class 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
62class 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
72class GenericXMLRecord(GenericTimestampedRecord):
73 FORMAT = '<II490sH'
74
75 @property
76 def xmldata(self):
77 data = self.data[2].replace("\x00", "")
78 return data
79
80
81class 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
99class 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
114class 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
149class 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 @@
1import crc16
2import struct
3
4class 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. 1import crc16
2 2import constants
3Copyright 2013 3import database_records
4 4import datetime
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16"""
17
18from curses import ascii as asciicc
19import re
20import serial 5import serial
21import sys 6import sys
22import time 7import time
8import packetwriter
9import struct
10import re
11import util
23import xml.etree.ElementTree as ET 12import xml.etree.ElementTree as ET
13import numpy
14import platform
15
16
17class 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
26class Dexcom(object): 31class 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
137d = 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
139if d.Handshake():
140 print "Connected successfully!"
141 257
142print d.IdentifyDevice() 258if __name__ == '__main__':
143print d.GetManufacturingParameters() 259 Dexcom.LocateAndDownload()
144print d.GetFirmwareHeader()
145print d.GetPcParams()
diff --git a/util.py b/util.py
new file mode 100644
index 0000000..c719092
--- /dev/null
+++ b/util.py
@@ -0,0 +1,82 @@
1import constants
2import datetime
3import os
4import platform
5import plistlib
6import re
7import subprocess
8
9
10def ReceiverTimeToTime(rtime):
11 return constants.BASE_TIME + datetime.timedelta(seconds=rtime)
12
13
14def 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
32def 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
65def 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())