diff options
author | Diego Elio Pettenò <flameeyes@flameeyes.eu> | 2013-08-03 10:07:41 +0200 |
---|---|---|
committer | Diego Elio Pettenò <flameeyes@flameeyes.eu> | 2013-08-03 10:07:41 +0200 |
commit | 389f424b0b541e581115d4cda355cd2c44118e1f (patch) | |
tree | b83b3b42d4ddb2be764ba841bbd725528d83c413 /glucometerutils/drivers/otultra2.py | |
download | glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar.gz glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar.bz2 glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar.lz glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar.xz glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.tar.zst glucometerutils-389f424b0b541e581115d4cda355cd2c44118e1f.zip |
Diffstat (limited to 'glucometerutils/drivers/otultra2.py')
-rw-r--r-- | glucometerutils/drivers/otultra2.py | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/glucometerutils/drivers/otultra2.py b/glucometerutils/drivers/otultra2.py new file mode 100644 index 0000000..43be6d4 --- /dev/null +++ b/glucometerutils/drivers/otultra2.py @@ -0,0 +1,257 @@ +"""Driver for LifeScan OneTouch Ultra 2 devices""" + +__author__ = 'Diego Elio Pettenò' +__email__ = 'flameeyes@flameeyes.eu' +__copyright__ = 'Copyright © 2013, Diego Elio Pettenò' +__license__ = 'GPL v3 or later' + +import datetime +import re + +import serial + +from glucometerutils import common +from glucometerutils import exceptions + + +class MissingChecksum(exceptions.InvalidResponse): + """The response misses the expected 4-digits checksum.""" + def __init__(self, response): + self.response = response + + def __str__(self): + return 'Response is missing the OT2 checksum: %s' % self.response + + +class InvalidSerialNumber(exceptions.Error): + """The serial number is not ending with Y as expected.""" + + +class Device(object): + def __init__(self, device): + self.serial_ = serial.Serial( + port=device, baudrate=9600, bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, + timeout=1, xonxoff=False, rtscts=False, dsrdtr=False, writeTimeout=None) + + def _SendCommand(self, cmd): + """Send command interface. + + Args: + cmd: command and parameters to send (without newline) + + This function exists to wrap the need to send the 0x11 0x0d prefix with + each command that wakes this model up. + """ + cmdstring = bytes('\x11\r' + cmd + '\r', 'ascii') + self.serial_.write(cmdstring); + self.serial_.flush() + + _RESPONSE_MATCH = re.compile(r'^(.+) ([0-9A-F]{4})\r$') + + def _ValidateAndStripChecksum(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 MissingChecksum(line) + + response, checksum = match.groups() + + # TODO(flameeyes) check that the checksum is actually valid + return response + + def _SendOnelinerCommand(self, cmd): + """Send command and read a one-line response. + + Args: + cmd: command and parameters to send (without newline) + + Returns: + A single line of text that the glucometer responds, without the checksum. + """ + self._SendCommand(cmd) + + line = self.serial_.readline().decode('ascii') + return self._ValidateAndStripChecksum(line) + + def GetVersion(self): + """Returns an identifier of the firmware version of the glucometer. + + Returns: + The software version returned by the glucometer, such as + "P02.00.00 30/08/06". + """ + response = self._SendOnelinerCommand('DM?') + + if response[0] != '?': + raise InvalidResponse(response) + + return response[1:] + + _SERIAL_NUMBER_RE = re.compile('^@ "([A-Z0-9]{9})"$') + + def GetSerialNumber(self): + """Retrieve the serial number of the device. + + Returns: + A string representing the serial number of the device. + + Raises: + exceptions.InvalidResponse: if the DM@ command returns a string not + matching _SERIAL_NUMBER_RE. + InvalidSerialNumber: if the returned serial number does not match + the OneTouch2 device as per specs. + """ + response = self._SendOnelinerCommand('DM@') + + match = self._SERIAL_NUMBER_RE.match(response) + if not match: + raise exceptions.InvalidResponse(response) + + serial_number = match.group(1) + + # 'Y' at the far right of the serial number is the indication of a OneTouch + # Ultra2 device, as per specs. + if serial_number[-1] != 'Y': + raise InvalidSerialNumber('Serial number %s is invalid.' % serial_number) + + 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 _ParseDateTime(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 GetDateTime(self): + """Returns the current date and time for the glucometer. + + Returns: + A datetime object built according to the returned response. + """ + response = self._SendOnelinerCommand('DMF') + return self._ParseDateTime(response[2:]) + + def SetDateTime(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. + """ + response = self._SendOnelinerCommand( + 'DMT' + date.strftime('%m/%d/%y %H:%M:%S')) + + return self._ParseDateTime(response[2:]) + + def _ParseGlucoseUnit(self, unit): + """Parses the value of a OneTouch Ultra Glucose unit definition. + + Args: + unit: the string reported by the glucometer as glucose unit. + + Return: + common.UNIT_MGDL: if the glucometer reads in mg/dL + common.UNIT_MMOLL: if the glucometer reads 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 GetGlucoseUnit(self): + """Returns a constant representing the unit for the dumped readings. + + Returns: + common.UNIT_MGDL: if the glucometer reads in mg/dL + common.UNIT_MMOLL: if the glucometer reads in mmol/L + """ + response = self._SendOnelinerCommand('DMSU?') + + match = self._GLUCOSE_UNIT_RE.match(response) + return self._ParseGlucoseUnit(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 ("[A-Z]{3}","[0-9/]{8}","[0-9:]{8} "),"([ 0-9.]{6})",') + + def GetReadings(self, unit=None): + """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. + """ + self._SendCommand('DMP') + data = self.serial_.readlines() + + header = data.pop(0).decode('ascii') + match = self._DUMP_HEADER_RE.match(header) + if not match: + raise exceptions.InvalidResponse(header) + + final_unit = unit or self._ParseGlucoseUnit(match.group(2)) + count = int(match.group(1)) + assert count == len(data) + + for line in data: + line = self._ValidateAndStripChecksum(line.decode('ascii')) + + match = self._DUMP_LINE_RE.match(line) + if not match: + raise exceptions.InvalidResponse(line) + + date = self._ParseDateTime(match.group(1)) + + # OneTouch2 always returns the data in mg/dL even if the + # glucometer is set to mmol/L. We need to convert it to the + # requested unit here. + value = common.ConvertGlucoseUnit(int(match.group(2)), + common.UNIT_MGDL, unit) + + yield (date, value) |