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/database_records.py | |
parent | 6b5ce2584b0fe2da991540a33c71b1f95cd8878a (diff) |
prep for installable module
Diffstat (limited to 'dexcom_reader/database_records.py')
-rw-r--r-- | dexcom_reader/database_records.py | 189 |
1 files changed, 189 insertions, 0 deletions
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) | ||