summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xglucometer.py1
-rw-r--r--glucometerutils/common.py4
-rw-r--r--glucometerutils/drivers/otultra2.py232
3 files changed, 115 insertions, 122 deletions
diff --git a/glucometer.py b/glucometer.py
index 5e05a77..155cd93 100755
--- a/glucometer.py
+++ b/glucometer.py
@@ -15,7 +15,6 @@ from dateutil import parser as date_parser
from glucometerutils import common
from glucometerutils import exceptions
-from glucometerutils.drivers import otultra2
def main():
parser = argparse.ArgumentParser()
diff --git a/glucometerutils/common.py b/glucometerutils/common.py
index 825dea1..38f735e 100644
--- a/glucometerutils/common.py
+++ b/glucometerutils/common.py
@@ -8,6 +8,8 @@ __license__ = 'MIT'
import collections
+from glucometerutils import exceptions
+
# Constants for units
UNIT_MGDL = 'mg/dL'
UNIT_MMOLL = 'mmol/L'
@@ -18,8 +20,6 @@ VALID_UNITS = [UNIT_MGDL, UNIT_MMOLL]
DATETIME_12HR = '12 hours'
DATETIME_24HR = '24 hours'
-from glucometerutils import exceptions
-
def convert_glucose_unit(value, from_unit, to_unit=None):
"""Convert the given value of glucose level between units.
diff --git a/glucometerutils/drivers/otultra2.py b/glucometerutils/drivers/otultra2.py
index 1a7b794..dc03c97 100644
--- a/glucometerutils/drivers/otultra2.py
+++ b/glucometerutils/drivers/otultra2.py
@@ -15,6 +15,93 @@ from glucometerutils import common
from glucometerutils import exceptions
from glucometerutils.drivers import lifescan_common
+# The following two hashes are taken directly from LifeScan's documentation
+_MEAL_CODES = {
+ 'N': '',
+ 'B': 'Before Meal',
+ 'A': 'After Meal',
+}
+
+_COMMENT_CODES = {
+ '00': '', # would be 'No Comment'
+ '01': 'Not Enough Food',
+ '02': 'Too Much Food',
+ '03': 'Mild Exercise',
+ '04': 'Hard Exercise',
+ '05': 'Medication',
+ '06': 'Stress',
+ '07': 'Illness',
+ '08': 'Feel Hypo',
+ '09': 'Menses',
+ '10': 'Vacation',
+ '11': 'Other',
+}
+
+_DUMP_HEADER_RE = re.compile(
+ r'P ([0-9]{3}),"[0-9A-Z]{9}","(?:MG/DL |MMOL/L)"')
+_DUMP_LINE_RE = re.compile(
+ r'P (?P<datetime>"[A-Z]{3}","[0-9/]{8}","[0-9:]{8} "),'
+ r'"(?P<control>[C ]) (?P<value>[0-9]{3})(?P<parityerror>[\? ])",'
+ r'"(?P<meal>[NBA])","(?P<comment>0[0-9]|1[01])", 00')
+
+_RESPONSE_MATCH = re.compile(r'^(.+) ([0-9A-F]{4})\r$')
+
+def _validate_and_strip_checksum(line):
+ """Verify the simple 16-bit checksum and remove it from the line.
+
+ Args:
+ line: the line to check the checksum of.
+
+ Returns:
+ A copy of the line with the checksum stripped out.
+ """
+ match = _RESPONSE_MATCH.match(line)
+
+ if not match:
+ raise lifescan_common.MissingChecksum(line)
+
+ response, checksum_string = match.groups()
+
+ try:
+ checksum_given = int(checksum_string, 16)
+ checksum_calculated = lifescan_common.calculate_checksum(
+ bytes(response, 'ascii'))
+
+ if checksum_given != checksum_calculated:
+ raise lifescan_common.InvalidChecksum(checksum_given,
+ checksum_calculated)
+ except ValueError:
+ raise lifescan_common.InvalidChecksum(checksum_given, None)
+
+ return response
+
+_DATETIME_RE = re.compile(
+ r'^"[A-Z]{3}","([0-9]{2}/[0-9]{2}/[0-9]{2})","([0-9]{2}:[0-9]{2}:[0-9]{2}) "$')
+
+
+def _parse_datetime(response):
+ """Convert a response with date and time from the meter into a datetime.
+
+ Args:
+ response: the response coming from a DMF or DMT command
+
+ Returns:
+ A datetime object built according to the returned response.
+
+ Raises:
+ InvalidResponse if the string cannot be matched by _DATETIME_RE.
+ """
+ match = _DATETIME_RE.match(response)
+ if not match:
+ raise exceptions.InvalidResponse(response)
+
+ date, time = match.groups()
+ month, day, year = [int(part) for part in date.split('/')]
+ hour, minute, second = [int(part) for part in time.split(':')]
+
+ # Yes, OneTouch2's firmware is not Y2K safe.
+ return datetime.datetime(2000 + year, month, day, hour, minute, second)
+
class Device(object):
def __init__(self, device):
@@ -33,41 +120,9 @@ class Device(object):
each command that wakes this model up.
"""
cmdstring = bytes('\x11\r' + cmd + '\r', 'ascii')
- self.serial_.write(cmdstring);
+ self.serial_.write(cmdstring)
self.serial_.flush()
- _RESPONSE_MATCH = re.compile(r'^(.+) ([0-9A-F]{4})\r$')
-
- def _validate_and_strip_checksum(self, line):
- """Verify the CRC16 checksum and remove it from the line.
-
- Args:
- line: the line to check the CRC16 of.
-
- Returns:
- A copy of the line with the CRC16 stripped out.
- """
- match = self._RESPONSE_MATCH.match(line)
-
- if not match:
- raise lifescan_common.MissingChecksum(line)
-
- response, checksum_string = match.groups()
-
- try:
- checksum_given = int(checksum_string, 16)
- checksum_calculated = lifescan_common.calculate_checksum(
- bytes(response, 'ascii'))
-
- if checksum_given != checksum_calculated:
- raise lifescan_common.InvalidChecksum(checksum_given,
- checksum_calculated)
- except ValueError:
- raise lifescan_common.InvalidChecksum(checksum_given,
- None)
-
- return response
-
def _send_oneliner_command(self, cmd):
"""Send command and read a one-line response.
@@ -80,7 +135,7 @@ class Device(object):
self._send_command(cmd)
line = self.serial_.readline().decode('ascii')
- return self._validate_and_strip_checksum(line)
+ return _validate_and_strip_checksum(line)
def get_information_string(self):
"""Returns a single string with all the identification information.
@@ -143,33 +198,6 @@ class Device(object):
return serial_number
- # The [TF] at the start is to accept both Get (F) and Set (T) commands.
- _DATETIME_RE = re.compile(
- r'^"[A-Z]{3}","([0-9]{2}/[0-9]{2}/[0-9]{2})","([0-9]{2}:[0-9]{2}:[0-9]{2}) "$')
-
- def _parse_datetime(self, response):
- """Convert a response with date and time from the meter into a datetime.
-
- Args:
- response: the response coming from a DMF or DMT command
-
- Returns:
- A datetime object built according to the returned response.
-
- Raises:
- InvalidResponse if the string cannot be matched by _DATETIME_RE.
- """
- match = self._DATETIME_RE.match(response)
- if not match:
- raise exceptions.InvalidResponse(response)
-
- date, time = match.groups()
- month, day, year = [int(part) for part in date.split('/')]
- hour, minute, second = [int(part) for part in time.split(':')]
-
- # Yes, OneTouch2's firmware is not Y2K safe.
- return datetime.datetime(2000 + year, month, day, hour, minute, second)
-
def get_datetime(self):
"""Returns the current date and time for the glucometer.
@@ -177,7 +205,7 @@ class Device(object):
A datetime object built according to the returned response.
"""
response = self._send_oneliner_command('DMF')
- return self._parse_datetime(response[2:])
+ return _parse_datetime(response[2:])
def set_datetime(self, date=datetime.datetime.now()):
"""Sets the date and time of the glucometer.
@@ -192,7 +220,7 @@ class Device(object):
response = self._send_oneliner_command(
'DMT' + date.strftime('%m/%d/%y %H:%M:%S'))
- return self._parse_datetime(response[2:])
+ return _parse_datetime(response[2:])
def zero_log(self):
"""Zeros out the data log of the device.
@@ -204,46 +232,34 @@ class Device(object):
if response != 'Z':
raise exceptions.InvalidResponse(response)
- def _parse_glucose_unit(self, unit):
-
- """Parses the value of a OneTouch Ultra Glucose unit definition.
+ _GLUCOSE_UNIT_RE = re.compile(r'^SU\?,"(MG/DL |MMOL/L)"')
- Args:
- unit: the string reported by the glucometer as glucose unit.
+ def get_glucose_unit(self):
+ """Returns a constant representing the unit displayed by the meter.
- Return:
- common.UNIT_MGDL: if the glucometer reads in mg/dL
- common.UNIT_MMOLL: if the glucometer reads in mmol/L
+ Returns:
+ common.UNIT_MGDL: if the glucometer displays in mg/dL
+ common.UNIT_MMOLL: if the glucometer displays in mmol/L
Raises:
exceptions.InvalidGlucoseUnit: if the unit is not recognized
- """
- if unit == 'MG/DL ':
- return common.UNIT_MGDL
- elif unit == 'MMOL/L':
- return common.UNIT_MMOLL
- else:
- raise exceptions.InvalidGlucoseUnit(string)
- _GLUCOSE_UNIT_RE = re.compile(r'^SU\?,"(MG/DL |MMOL/L)"')
-
- def get_glucose_unit(self):
- """Returns a constant representing the unit for the dumped readings.
+ OneTouch meters will always dump data in mg/dL because that's their internal
+ storage. They will then provide a separate method to read the unit used for
+ display. This is not settable by the user in all modern meters.
- Returns:
- common.UNIT_MGDL: if the glucometer reads in mg/dL
- common.UNIT_MMOLL: if the glucometer reads in mmol/L
"""
response = self._send_oneliner_command('DMSU?')
match = self._GLUCOSE_UNIT_RE.match(response)
- return self._parse_glucose_unit(match.group(1))
+ unit = match.group(1)
- _DUMP_HEADER_RE = re.compile(r'P ([0-9]{3}),"[0-9A-Z]{9}","(?:MG/DL |MMOL/L)"')
- _DUMP_LINE_RE = re.compile(
- r'P (?P<datetime>"[A-Z]{3}","[0-9/]{8}","[0-9:]{8} "),'
- r'"(?P<control>[C ]) (?P<value>[0-9]{3})(?P<parityerror>[\? ])",'
- r'"(?P<meal>[NBA])","(?P<comment>0[0-9]|1[01])", 00')
+ if unit == 'MG/DL ':
+ return common.UNIT_MGDL
+ elif unit == 'MMOL/L':
+ return common.UNIT_MMOLL
+ else:
+ raise exceptions.InvalidGlucoseUnit(string)
def get_readings(self):
"""Iterates over the reading values stored in the glucometer.
@@ -263,7 +279,7 @@ class Device(object):
data = self.serial_.readlines()
header = data.pop(0).decode('ascii')
- match = self._DUMP_HEADER_RE.match(header)
+ match = _DUMP_HEADER_RE.match(header)
if not match:
raise exceptions.InvalidResponse(header)
@@ -271,41 +287,19 @@ class Device(object):
assert count == len(data)
for line in data:
- line = self._validate_and_strip_checksum(line.decode('ascii'))
+ line = _validate_and_strip_checksum(line.decode('ascii'))
- match = self._DUMP_LINE_RE.match(line)
+ match = _DUMP_LINE_RE.match(line)
if not match:
raise exceptions.InvalidResponse(line)
line_data = match.groupdict()
- date = self._parse_datetime(line_data['datetime'])
- meal = self._MEAL_CODES[line_data['meal']]
- comment = self._COMMENT_CODES[line_data['comment']]
+ date = _parse_datetime(line_data['datetime'])
+ meal = _MEAL_CODES[line_data['meal']]
+ comment = _COMMENT_CODES[line_data['comment']]
# OneTouch2 always returns the data in mg/dL even if the glucometer is set
# to mmol/L, so there is no conversion required.
yield common.Reading(
date, float(line_data['value']), meal=meal, comment=comment)
-
- # The following two hashes are taken directly from LifeScan's documentation
- _MEAL_CODES = {
- 'N': '',
- 'B': 'Before Meal',
- 'A': 'After Meal',
- }
-
- _COMMENT_CODES = {
- '00': '', # would be 'No Comment'
- '01': 'Not Enough Food',
- '02': 'Too Much Food',
- '03': 'Mild Exercise',
- '04': 'Hard Exercise',
- '05': 'Medication',
- '06': 'Stress',
- '07': 'Illness',
- '08': 'Feel Hypo',
- '09': 'Menses',
- '10': 'Vacation',
- '11': 'Other',
- }