diff options
author | Ben West <bewest@gmail.com> | 2016-05-28 19:31:45 -0700 |
---|---|---|
committer | Ben West <bewest@gmail.com> | 2016-05-28 19:31:45 -0700 |
commit | 676a61177cf157d0d3a736e1141e52c1bf328f15 (patch) | |
tree | f0ee27234e21a54291e07356d6801e7cc16fe150 | |
parent | 12751d803cde318b75f848940c99889719129acd (diff) | |
parent | 0193c69e36cfb079a5cefd56518215c4cef103ec (diff) |
Merge pull request #3 from openaps/dev
stub out next release: 0.1.0-dev area
-rw-r--r-- | dexcom_reader/database_records.py | 112 | ||||
-rw-r--r-- | dexcom_reader/readdata.py | 28 | ||||
-rw-r--r-- | etc/udev/rules.d/80-dexcom.rules | 18 | ||||
-rw-r--r-- | setup.py | 6 |
4 files changed, 156 insertions, 8 deletions
diff --git a/dexcom_reader/database_records.py b/dexcom_reader/database_records.py index 8d471f4..9274d24 100644 --- a/dexcom_reader/database_records.py +++ b/dexcom_reader/database_records.py | |||
@@ -103,21 +103,117 @@ class InsertionRecord(GenericTimestampedRecord): | |||
103 | def session_state(self): | 103 | def session_state(self): |
104 | states = [None, 'REMOVED', 'EXPIRED', 'RESIDUAL_DEVIATION', | 104 | states = [None, 'REMOVED', 'EXPIRED', 'RESIDUAL_DEVIATION', |
105 | 'COUNTS_DEVIATION', 'SECOND_SESSION', 'OFF_TIME_LOSS', | 105 | 'COUNTS_DEVIATION', 'SECOND_SESSION', 'OFF_TIME_LOSS', |
106 | 'STARTED', 'BAD_TRANSMITTER', 'MANUFACTURING_MODE'] | 106 | 'STARTED', 'BAD_TRANSMITTER', 'MANUFACTURING_MODE', |
107 | 'UNKNOWN1', 'UNKNOWN2', 'UNKNOWN3', 'UNKNOWN4', 'UNKNOWN5', | ||
108 | 'UNKNOWN6', 'UNKNOWN7', 'UNKNOWN8'] | ||
107 | return states[ord(self.data[3])] | 109 | return states[ord(self.data[3])] |
108 | 110 | ||
109 | def __repr__(self): | 111 | def __repr__(self): |
110 | return '%s: state=%s' % (self.display_time, self.session_state) | 112 | return '%s: state=%s' % (self.display_time, self.session_state) |
111 | 113 | ||
114 | class G5InsertionRecord (InsertionRecord): | ||
115 | FORMAT = '<3Ic10BH' | ||
112 | 116 | ||
113 | class Calibration(GenericTimestampedRecord): | 117 | class Calibration(GenericTimestampedRecord): |
118 | FORMAT = '<2Iddd3cdb' | ||
119 | # CAL_FORMAT = '<2Iddd3cdb' | ||
120 | FIELDS = [ 'slope', 'intercept', 'scale', 'decay', 'numsub', 'raw' ] | ||
114 | @property | 121 | @property |
115 | def raw(self): | 122 | def raw (self): |
116 | return binascii.hexlify(bytearray(self.data)) | 123 | return binascii.hexlify(self.raw_data) |
124 | @property | ||
125 | def slope (self): | ||
126 | return self.data[2] | ||
127 | @property | ||
128 | def intercept (self): | ||
129 | return self.data[3] | ||
130 | @property | ||
131 | def scale (self): | ||
132 | return self.data[4] | ||
133 | @property | ||
134 | def decay (self): | ||
135 | return self.data[8] | ||
136 | @property | ||
137 | def numsub (self): | ||
138 | return int(self.data[9]) | ||
117 | 139 | ||
118 | def __repr__(self): | 140 | def __repr__(self): |
119 | return '%s: CAL SET:%s' % (self.display_time, self.raw) | 141 | return '%s: CAL SET:%s' % (self.display_time, self.raw) |
120 | 142 | ||
143 | LEGACY_SIZE = 148 | ||
144 | REV_2_SIZE = 249 | ||
145 | @classmethod | ||
146 | def _ClassSize(cls): | ||
147 | |||
148 | return cls.REV_2_SIZE | ||
149 | |||
150 | @classmethod | ||
151 | def Create(cls, data, record_counter): | ||
152 | offset = record_counter * cls._ClassSize() | ||
153 | cal_size = struct.calcsize(cls.FORMAT) | ||
154 | raw_data = data[offset:offset + cls._ClassSize()] | ||
155 | |||
156 | cal_data = data[offset:offset + cal_size] | ||
157 | unpacked_data = cls._ClassFormat().unpack(cal_data) | ||
158 | return cls(unpacked_data, raw_data) | ||
159 | |||
160 | def __init__ (self, data, raw_data): | ||
161 | self.page_data = raw_data | ||
162 | self.raw_data = raw_data | ||
163 | self.data = data | ||
164 | subsize = struct.calcsize(SubCal.FORMAT) | ||
165 | offset = self.numsub * subsize | ||
166 | calsize = struct.calcsize(self.FORMAT) | ||
167 | caldata = raw_data[:calsize] | ||
168 | subdata = raw_data[calsize:calsize + offset] | ||
169 | crcdata = raw_data[calsize+offset:calsize+offset+2] | ||
170 | |||
171 | subcals = [ ] | ||
172 | for i in xrange(self.numsub): | ||
173 | offset = i * subsize | ||
174 | raw_sub = subdata[offset:offset+subsize] | ||
175 | sub = SubCal(raw_sub, self.data[1]) | ||
176 | subcals.append(sub) | ||
177 | |||
178 | self.subcals = subcals | ||
179 | |||
180 | self.check_crc() | ||
181 | def to_dict (self): | ||
182 | res = super(Calibration, self).to_dict( ) | ||
183 | res['subrecords'] = [ sub.to_dict( ) for sub in self.subcals ] | ||
184 | return res | ||
185 | @property | ||
186 | def crc(self): | ||
187 | return struct.unpack('H', self.raw_data[-2:])[0] | ||
188 | |||
189 | class LegacyCalibration (Calibration): | ||
190 | @classmethod | ||
191 | def _ClassSize(cls): | ||
192 | |||
193 | return cls.LEGACY_SIZE | ||
194 | |||
195 | |||
196 | class SubCal (GenericTimestampedRecord): | ||
197 | FORMAT = '<IIIIc' | ||
198 | BASE_FIELDS = [ ] | ||
199 | FIELDS = [ 'entered', 'meter', 'sensor', 'applied', ] | ||
200 | def __init__ (self, raw_data, displayOffset=None): | ||
201 | self.raw_data = raw_data | ||
202 | self.data = self._ClassFormat().unpack(raw_data) | ||
203 | self.displayOffset = displayOffset | ||
204 | @property | ||
205 | def entered (self): | ||
206 | return util.ReceiverTimeToTime(self.data[0]) | ||
207 | @property | ||
208 | def meter (self): | ||
209 | return int(self.data[1]) | ||
210 | @property | ||
211 | def sensor (self): | ||
212 | return int(self.data[2]) | ||
213 | @property | ||
214 | def applied (self): | ||
215 | return util.ReceiverTimeToTime(self.data[3]) | ||
216 | |||
121 | class MeterRecord(GenericTimestampedRecord): | 217 | class MeterRecord(GenericTimestampedRecord): |
122 | FORMAT = '<2IHIH' | 218 | FORMAT = '<2IHIH' |
123 | FIELDS = ['meter_glucose', 'meter_time' ] | 219 | FIELDS = ['meter_glucose', 'meter_time' ] |
@@ -133,6 +229,8 @@ class MeterRecord(GenericTimestampedRecord): | |||
133 | def __repr__(self): | 229 | def __repr__(self): |
134 | return '%s: Meter BG:%s' % (self.display_time, self.meter_glucose) | 230 | return '%s: Meter BG:%s' % (self.display_time, self.meter_glucose) |
135 | 231 | ||
232 | class G5MeterRecord (MeterRecord): | ||
233 | FORMAT = '<2IHI5BH' | ||
136 | 234 | ||
137 | class EventRecord(GenericTimestampedRecord): | 235 | class EventRecord(GenericTimestampedRecord): |
138 | # sys_time,display_time,glucose,meter_time,crc | 236 | # sys_time,display_time,glucose,meter_time,crc |
@@ -230,3 +328,11 @@ class EGVRecord(GenericTimestampedRecord): | |||
230 | else: | 328 | else: |
231 | return '%s: CGM BG:%s (%s) DO:%s' % (self.display_time, self.glucose, | 329 | return '%s: CGM BG:%s (%s) DO:%s' % (self.display_time, self.glucose, |
232 | self.trend_arrow, self.display_only) | 330 | self.trend_arrow, self.display_only) |
331 | |||
332 | class G5EGVRecord (EGVRecord): | ||
333 | FORMAT = '<2IHBBBBBBBBBcBH' | ||
334 | @property | ||
335 | def full_trend(self): | ||
336 | return self.data[12] | ||
337 | |||
338 | |||
diff --git a/dexcom_reader/readdata.py b/dexcom_reader/readdata.py index cc02de3..0f20e71 100644 --- a/dexcom_reader/readdata.py +++ b/dexcom_reader/readdata.py | |||
@@ -251,7 +251,7 @@ class Dexcom(object): | |||
251 | assert ord(packet.command) == 1 | 251 | assert ord(packet.command) == 1 |
252 | # first index (uint), numrec (uint), record_type (byte), revision (byte), | 252 | # first index (uint), numrec (uint), record_type (byte), revision (byte), |
253 | # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc) | 253 | # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc) |
254 | header_format = '<2I2c4IH' | 254 | header_format = '<2IcB4IH' |
255 | header_data_len = struct.calcsize(header_format) | 255 | header_data_len = struct.calcsize(header_format) |
256 | header = struct.unpack_from(header_format, packet.data) | 256 | header = struct.unpack_from(header_format, packet.data) |
257 | header_crc = crc16.crc16(packet.data[:header_data_len-2]) | 257 | header_crc = crc16.crc16(packet.data[:header_data_len-2]) |
@@ -266,15 +266,21 @@ class Dexcom(object): | |||
266 | for x in xrange(header[1]): | 266 | for x in xrange(header[1]): |
267 | yield record_type.Create(data, x) | 267 | yield record_type.Create(data, x) |
268 | 268 | ||
269 | def ParsePage(self, header, data): | 269 | PARSER_MAP = { |
270 | record_type = constants.RECORD_TYPES[ord(header[2])] | ||
271 | generic_parser_map = { | ||
272 | 'USER_EVENT_DATA': database_records.EventRecord, | 270 | 'USER_EVENT_DATA': database_records.EventRecord, |
273 | 'METER_DATA': database_records.MeterRecord, | 271 | 'METER_DATA': database_records.MeterRecord, |
272 | 'CAL_SET': database_records.Calibration, | ||
273 | # 'CAL_SET': database_records.Calibration, | ||
274 | 'INSERTION_TIME': database_records.InsertionRecord, | 274 | 'INSERTION_TIME': database_records.InsertionRecord, |
275 | 'EGV_DATA': database_records.EGVRecord, | 275 | 'EGV_DATA': database_records.EGVRecord, |
276 | 'SENSOR_DATA': database_records.SensorRecord, | 276 | 'SENSOR_DATA': database_records.SensorRecord, |
277 | } | 277 | } |
278 | def ParsePage(self, header, data): | ||
279 | record_type = constants.RECORD_TYPES[ord(header[2])] | ||
280 | revision = int(header[3]) | ||
281 | generic_parser_map = self.PARSER_MAP | ||
282 | if revision < 2 and record_type == 'CAL_SET': | ||
283 | generic_parser_map.update(CAL_SET=database_records.LegacyCalibration) | ||
278 | xml_parsed = ['PC_SOFTWARE_PARAMETER', 'MANUFACTURING_DATA'] | 284 | xml_parsed = ['PC_SOFTWARE_PARAMETER', 'MANUFACTURING_DATA'] |
279 | if record_type in generic_parser_map: | 285 | if record_type in generic_parser_map: |
280 | return self.GenericRecordYielder(header, data, | 286 | return self.GenericRecordYielder(header, data, |
@@ -308,6 +314,20 @@ class Dexcom(object): | |||
308 | records.extend(self.ReadDatabasePage(record_type, x)) | 314 | records.extend(self.ReadDatabasePage(record_type, x)) |
309 | return records | 315 | return records |
310 | 316 | ||
317 | class DexcomG5 (Dexcom): | ||
318 | PARSER_MAP = { | ||
319 | 'USER_EVENT_DATA': database_records.EventRecord, | ||
320 | 'METER_DATA': database_records.G5MeterRecord, | ||
321 | 'CAL_SET': database_records.Calibration, | ||
322 | 'INSERTION_TIME': database_records.G5InsertionRecord, | ||
323 | 'EGV_DATA': database_records.G5EGVRecord, | ||
324 | 'SENSOR_DATA': database_records.SensorRecord, | ||
325 | } | ||
326 | |||
327 | def GetDevice (port, G5=False): | ||
328 | if G5: | ||
329 | return DexcomG5(port) | ||
330 | return Dexcom(port) | ||
311 | 331 | ||
312 | if __name__ == '__main__': | 332 | if __name__ == '__main__': |
313 | Dexcom.LocateAndDownload() | 333 | Dexcom.LocateAndDownload() |
diff --git a/etc/udev/rules.d/80-dexcom.rules b/etc/udev/rules.d/80-dexcom.rules new file mode 100644 index 0000000..0c95f52 --- /dev/null +++ b/etc/udev/rules.d/80-dexcom.rules | |||
@@ -0,0 +1,18 @@ | |||
1 | |||
2 | |||
3 | |||
4 | # udev can only match one entry at a time, but this triggers additional add entries. | ||
5 | # 22a3:0047 | ||
6 | # 22a3 0047 | ||
7 | ATTRS{idVendor}=="22a3", ATTRS{idProduct}=="0047", \ | ||
8 | ENV{ID_MM_DEVICE_IGNORE}="1", \ | ||
9 | MODE="0664", GROUP="plugdev" | ||
10 | |||
11 | |||
12 | # create a symlink to the ttyUSB device created automatically above | ||
13 | #ACTION=="add", \ | ||
14 | # SUBSYSTEM=="tty", \ | ||
15 | # ATTRS{idVendor}=="22a3", ATTRS{idProduct}=="0047", \ | ||
16 | # SYMLINK+="ttyUSB.Dexcom%n" | ||
17 | |||
18 | |||
@@ -10,7 +10,7 @@ def readme(): | |||
10 | return f.read() | 10 | return f.read() |
11 | 11 | ||
12 | setup(name='dexcom_reader', | 12 | setup(name='dexcom_reader', |
13 | version='0.0.8', # http://semver.org/ | 13 | version='0.1.0-dev', # http://semver.org/ |
14 | description='Audit, and inspect data from Dexcom G4.', | 14 | description='Audit, and inspect data from Dexcom G4.', |
15 | long_description=readme(), | 15 | long_description=readme(), |
16 | author="Will Nowak", | 16 | author="Will Nowak", |
@@ -34,6 +34,10 @@ setup(name='dexcom_reader', | |||
34 | package_data={ | 34 | package_data={ |
35 | 'dexcom_reader': ['etc/udev/rules.d/*'] | 35 | 'dexcom_reader': ['etc/udev/rules.d/*'] |
36 | }, | 36 | }, |
37 | data_files = [ | ||
38 | ('/etc/udev/rules.d/80-dexcom.rules', ['etc/udev/80-dexcom.rules'] ), | ||
39 | |||
40 | ], | ||
37 | zip_safe=False, | 41 | zip_safe=False, |
38 | include_package_data=True | 42 | include_package_data=True |
39 | ) | 43 | ) |