summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--glucometerutils/drivers/freestyle_optium.py249
-rw-r--r--glucometerutils/exceptions.py7
2 files changed, 256 insertions, 0 deletions
diff --git a/glucometerutils/drivers/freestyle_optium.py b/glucometerutils/drivers/freestyle_optium.py
new file mode 100644
index 0000000..0be4995
--- /dev/null
+++ b/glucometerutils/drivers/freestyle_optium.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+"""Driver for FreeStyle Optium devices"""
+
+__author__ = 'Diego Elio Pettenò'
+__email__ = 'flameeyes@flameeyes.eu'
+__copyright__ = 'Copyright © 2016, Diego Elio Pettenò'
+__license__ = 'MIT'
+
+import datetime
+import re
+import sys
+
+import serial
+
+from glucometerutils import common
+from glucometerutils import exceptions
+
+_CLOCK_RE = re.compile(
+ r'^Clock:\t(?P<month>[A-Z][a-z]{2}) (?P<day>[0-9]{2}) (?P<year>[0-9]{4})\t'
+ r'(?P<time>[0-9]{2}:[0-9]{2}:[0-9]{2})$')
+
+# The reading can be HI (padded to three-characters by a space) if the value was
+# over what the meter was supposed to read. Unlike the "Clock:" line, the months
+# of June and July are written in full, everything else is truncated to three
+# characters, so accept a space or 'e'/'y' at the end of the month name. Also,
+# the time does *not* include seconds. Note that the G is likely going to
+# indicate a blood-glucose reading rather than a blood-β-ketones. Unfortunately
+# I don't have β-ketone strips to test.
+_READING_RE = re.compile(
+ r'^(?P<reading>HI |[0-9]{3}) (?P<month>[A-Z][a-z]{2})[ ey] (?P<day>[0-9]{2}) '
+ r'(?P<year>[0-9]{4}) (?P<time>[0-9]{2}:[0-9]{2}) G 0x00')
+
+# There are two date format used by the device. One uses three-letters month
+# names, and that's easy enough. The other uses three-letters month names,
+# except for (at least) July. So ignore the fourth character.
+# explicit mapping. Note that the mapping *requires* a trailing whitespace.
+_MONTH_MATCHES = {
+ 'Jan': 1,
+ 'Feb': 2,
+ 'Mar': 3,
+ 'Apr': 4,
+ 'May': 5,
+ 'Jun': 6,
+ 'Jul': 7,
+ 'Aug': 8,
+ 'Sep': 9,
+ 'Oct': 10,
+ 'Nov': 11,
+ 'Dec': 12
+}
+
+
+def _parse_clock(datestr):
+ """Convert the date/time string used by the the device into a datetime.
+
+ Args:
+ datestr: a string as returned by the device during information handling.
+ """
+ match = _CLOCK_RE.match(datestr)
+ if not match:
+ raise exceptions.InvalidResponse(datestr)
+
+ # int() parses numbers in decimal, so we don't have to worry about '08'
+ day = int(match.group('day'))
+ month = _MONTH_MATCHES[match.group('month')]
+ year = int(match.group('year'))
+
+ hour, minute, second = (int(part) for part in match.group('time').split(':'))
+
+ return datetime.datetime(year, month, day, hour, minute, second)
+
+
+class Device(object):
+ def __init__(self, device):
+ self.serial_ = serial.Serial(
+ port=device, baudrate=19200, bytesize=serial.EIGHTBITS,
+ parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
+ timeout=1, xonxoff=True, rtscts=False, dsrdtr=False, writeTimeout=None)
+
+ def _send_command(self, command):
+ cmd_bytes = bytes('$%s\r\n' % command, 'ascii')
+ self.serial_.write(cmd_bytes)
+ self.serial_.flush()
+
+ response = self.serial_.readlines()
+ # We always want to decode the output, and remove stray \r\n. Any failure in
+ # decoding means the output is invalid anyway.
+ decoded_response = [line.decode('ascii').rstrip('\r\n')
+ for line in response]
+ return decoded_response
+
+ def connect(self):
+ self._send_command('xmem') # ignore output this time
+ self._fetch_device_information()
+
+ def disconnect(self):
+ return
+
+ def _fetch_device_information(self):
+ data = self._send_command('colq')
+
+ for line in data:
+ parsed_line = line.split('\t')
+
+ if parsed_line[0] == 'S/N:':
+ self.device_serialno_ = parsed_line[1]
+ elif parsed_line[0] == 'Ver:':
+ self.device_version_ = parsed_line[1]
+ if parsed_line[2] == 'MMOL':
+ self.device_glucose_unit_ = common.UNIT_MMOLL
+ else: # I only have a mmol/l device, so I can't be sure.
+ self.device_glucose_unit_ = common.UNIT_MGDL
+ # There are more entries: Clock, Market, ROM and Usage, but we don't care
+ # for those here.
+ elif parsed_line[0] == 'CMD OK':
+ return
+
+ # I have not figured out why this happens, but sometimes it's echoing back
+ # the commands and not replying to them.
+ raise exceptions.ConnectionFailed()
+
+ def get_information_string(self):
+ """Returns a single string with all the identification information.
+
+ Returns:
+ A string including the serial number, software version, date and time and
+ default unit.
+ """
+ return ('Freestyle Optium glucometer\n'
+ 'Serial number: %s\n'
+ 'Software version: %s\n'
+ 'Time: %s\n'
+ 'Default unit: %s' % (
+ self.get_serial_number(),
+ self.get_version(),
+ self.get_datetime(),
+ self.get_glucose_unit()))
+
+ def get_version(self):
+ """Returns an identifier of the firmware version of the glucometer.
+
+ Returns:
+ The software version returned by the glucometer, such as "0.22"
+ """
+ return self.device_version_
+
+ def get_serial_number(self):
+ """Retrieve the serial number of the device.
+
+ Returns:
+ A string representing the serial number of the device.
+ """
+ return self.device_serialno_
+
+ def get_glucose_unit(self):
+ """Returns a constant representing the unit displayed by the meter.
+
+ Returns:
+ common.UNIT_MGDL: if the glucometer displays in mg/dL
+ common.UNIT_MMOLL: if the glucometer displays in mmol/L
+ """
+ return self.device_glucose_unit_
+
+ def get_datetime(self):
+ """Returns the current date and time for the glucometer.
+
+ Returns:
+ A datetime object built according to the returned response.
+ """
+ data = self._send_command('colq')
+
+ for line in data:
+ if not line.startswith('Clock:'):
+ continue
+
+ return _parse_clock(line)
+
+ raise exceptions.InvalidResponse('\n'.join(data))
+
+ def set_datetime(self, date=datetime.datetime.now()):
+ """Sets the date and time of the glucometer.
+
+ Args:
+ date: The value to set the date/time of the glucometer to. If none is
+ given, the current date and time of the computer is used.
+
+ Returns:
+ A datetime object built according to the returned response.
+ """
+ data = self._send_command(date.strftime('tim,%m,%d,%y,%H,%M'))
+
+ parsed_data = ''.join(data)
+ if parsed_data != 'CMD OK':
+ raise exceptions.InvalidResponse(parsed_data)
+
+ return self.get_datetime()
+
+ def zero_log(self):
+ """Zeros out the data log of the device.
+
+ This function will clear the memory of the device deleting all the readings
+ in an irrecoverable way.
+ """
+ raise NotImplementedError
+
+ def get_readings(self):
+ """Iterates over the reading values stored in the glucometer.
+
+ Args:
+ unit: The glucose unit to use for the output.
+
+ Yields:
+ A tuple (date, value) of the readings in the glucometer. The value is a
+ floating point in the unit specified; if no unit is specified, the default
+ unit in the glucometer will be used.
+
+ Raises:
+ exceptions.InvalidResponse: if the response does not match what expected.
+ """
+ data = self._send_command('xmem')
+
+ # The first line is empty, the second is the serial number, the third the
+ # version, the fourth the current time, and the fifth the record count.. The
+ # last line has a checksum and the end.
+ count = int(data[4])
+ if count != (len(data) - 6):
+ raise exceptions.InvalidResponse('\n'.join(data))
+
+ for line in data[5:-1]:
+ match = _READING_RE.match(line)
+ if not match:
+ raise exceptions.InvalidResponse(line)
+
+ if match.group('reading') == 'HI ':
+ value = float("inf")
+ else:
+ value = float(match.group('reading'))
+
+ day = int(match.group('day'))
+ month = _MONTH_MATCHES[match.group('month')]
+ year = int(match.group('year'))
+
+ hour, minute = (int(part) for part in match.group('time').split(':'))
+
+ timestamp = datetime.datetime(year, month, day, hour, minute)
+
+ # The reading, if present, is always in mg/dL even if the glucometer is
+ # set to mmol/L.
+ yield common.Reading(timestamp, value)
diff --git a/glucometerutils/exceptions.py b/glucometerutils/exceptions.py
index 9802004..3e73048 100644
--- a/glucometerutils/exceptions.py
+++ b/glucometerutils/exceptions.py
@@ -13,6 +13,13 @@ class Error(Exception):
return self.message
+class ConnectionFailed(Error):
+ """It was not possible to connect to the meter."""
+
+ def __init__(self):
+ self.message = 'Unable to connect to the meter.'
+
+
class InvalidResponse(Error):
"""The response received from the meter was not understood"""