summaryrefslogblamecommitdiffstats
path: root/glucometerutils/drivers/otultra2.py
blob: 19166ed4c761f9aa20a06c4d6ac2949dcc2a4fd1 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
                       










                                                                      

                                  
                                     
                                                             
                   



               

                                      
                                            
                                          
 

                                                                           


                            


                  











                                      


                             
                                                      
                           


                                                                     


                                                       
                                    
                                                                       
 

                                                                        
 

                                                                                
 



                                                                  
 

                                             
 
                   
 
                                       
                                                                     
 

                                              
 



                                                        
 

                                            
 
                                              
 


                                                  
                                     
 




                                                                 
 
                   

                          

                                                                         


                              
                                                                            
 

                                                             
 

                                                                 
 





                                                                      
 


                                                    
 

                                                                           
 
 
                                  

                                                           
 
                                                     
              
 
                                                        
              
 

                                  
 





                                                               
 

                                                     
 

                                                               
 
                

                                                                         

                               
 

                                                      
 

                                                   
 



                                    




                                                            
 

                                                                           
 




                                                                  
 

                                                      
 
                           
 
                                                         
 

                                                    
 

                                                                
 






                                                                             
 


                                                      
 
                                      
 

                                                                          

                                                             
 
                            
 

                                                                
 




                                                                     
 

                                                         
 


                                                                               
 



                                                                     
                                                       
 
                                            
 

                                                
 

                                                                          



                                                      
 
                                                             
 

                                                                           
 


                                                                  
 

                                                                      
 



                                                                              

                                                       
 

                                                     
 

                                    

                            
                                     

                                                     
 

                                                                     
 

                                                       
 
               
                                                              
 
               


                                                                         


                                       
 



                                                    
 

                                   
 

                                                                     
 


                                                      
 
                                         
 


                                                          
 

                                                                               
                                        
                                                                            
# -*- coding: utf-8 -*-
"""Driver for LifeScan OneTouch Ultra 2 devices.

Supported features:
    - get readings, including pre-/post-meal notes and other comments;
    - use the glucose unit preset on the device by default;
    - get and set date and time;
    - get serial number and software version;
    - memory reset (caution!)

Expected device path: /dev/ttyUSB0 or similar serial port device.
"""

__author__ = 'Diego Elio Pettenò'
__email__ = 'flameeyes@flameeyes.com'
__copyright__ = 'Copyright © 2013-2017, Diego Elio Pettenò'
__license__ = 'MIT'

import datetime
import re

from glucometerutils import common
from glucometerutils import exceptions
from glucometerutils.support import lifescan
from glucometerutils.support import serial

# The following two hashes are taken directly from LifeScan's documentation
_MEAL_CODES = {
    'N': common.Meal.NONE,
    'B': common.Meal.BEFORE,
    'A': common.Meal.AFTER,
}

_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 _calculate_checksum(bytestring):
    """Calculate the checksum used by OneTouch Ultra and Ultra2 devices

    Args:
      bytestring: the string of which the checksum has to be calculated.

    Returns:
      A string with the hexdecimal representation of the checksum for the input.

    The checksum is a very stupid one: it just sums all the bytes,
    modulo 16-bit, without any parity.
    """
    checksum = 0

    for byte in bytestring:
        checksum = (checksum + byte) & 0xffff

    return checksum

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.MissingChecksum(line)

    response, checksum_string = match.groups()

    try:
        checksum_given = int(checksum_string, 16)
        checksum_calculated = _calculate_checksum(
            bytes(response, 'ascii'))

        if checksum_given != checksum_calculated:
            raise exceptions.InvalidChecksum(checksum_given,
                                             checksum_calculated)
    except ValueError:
        raise exceptions.InvalidChecksum(checksum_given, None)

    return response

_DATETIME_RE = re.compile(
    r'^"[A-Z]{3}",'
    r'"([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 = map(int, date.split('/'))
    hour, minute, second = map(int, time.split(':'))

    # Yes, OneTouch2's firmware is not Y2K safe.
    return datetime.datetime(2000 + year, month, day, hour, minute, second)


class Device(serial.SerialDevice):
    BAUDRATE = 9600
    DEFAULT_CABLE_ID = '067b:2303'  # Generic PL2303 cable.

    def connect(self):  # pylint: disable=no-self-use
        return

    def disconnect(self):  # pylint: disable=no-self-use
        return

    def _send_command(self, cmd):
        """Send command interface.

        Args:
          cmd: command and parameters to send (without newline)
        """
        cmdstring = bytes('\x11\r' + cmd + '\r', 'ascii')
        self.serial_.write(cmdstring)
        self.serial_.flush()

    def _send_oneliner_command(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._send_command(cmd)

        line = self.serial_.readline().decode('ascii')
        return _validate_and_strip_checksum(line)

    def get_meter_info(self):
        """Fetch and parses the device information.

        Returns:
          A common.MeterInfo object.
        """
        return common.MeterInfo(
            'OneTouch Ultra 2 glucometer',
            serial_number=self.get_serial_number(),
            version_info=(
                'Software version: ' + self.get_version(),),
            native_unit=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
            "P02.00.00 30/08/06".
        """
        response = self._send_oneliner_command('DM?')

        if response[0] != '?':
            raise exceptions.InvalidResponse(response)

        return response[1:]

    _SERIAL_NUMBER_RE = re.compile('^@ "([A-Z0-9]{9})"$')

    def get_serial_number(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._send_oneliner_command('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 lifescan.InvalidSerialNumber(serial_number)

        return serial_number

    def get_datetime(self):
        """Returns the current date and time for the glucometer.

        Returns:
          A datetime object built according to the returned response.
        """
        response = self._send_oneliner_command('DMF')
        return _parse_datetime(response[2:])

    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.
        """
        response = self._send_oneliner_command(
            'DMT' + date.strftime('%m/%d/%y %H:%M:%S'))

        return _parse_datetime(response[2:])

    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.
        """
        response = self._send_oneliner_command('DMZ')
        if response != 'Z':
            raise exceptions.InvalidResponse(response)

    _GLUCOSE_UNIT_RE = re.compile(r'^SU\?,"(MG/DL |MMOL/L)"')

    def get_glucose_unit(self):
        """Returns a constant representing the unit displayed by the meter.

        Returns:
          common.Unit.MG_DL: if the glucometer displays in mg/dL
          common.Unit.MMOL_L: if the glucometer displays in mmol/L

        Raises:
          exceptions.InvalidGlucoseUnit: if the unit is not recognized

        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.
        """
        response = self._send_oneliner_command('DMSU?')

        match = self._GLUCOSE_UNIT_RE.match(response)
        unit = match.group(1)

        if unit == 'MG/DL ':
            return common.Unit.MG_DL

        if unit == 'MMOL/L':
            return common.Unit.MMOL_L

        raise exceptions.InvalidGlucoseUnit(response)

    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 GlucoseReading object representing the read value.

        Raises:
          exceptions.InvalidResponse: if the response does not match what
          expected.

        """
        self._send_command('DMP')
        data = self.serial_.readlines()

        header = data.pop(0).decode('ascii')
        match = _DUMP_HEADER_RE.match(header)
        if not match:
            raise exceptions.InvalidResponse(header)

        count = int(match.group(1))
        assert count == len(data)

        for line in data:
            line = _validate_and_strip_checksum(line.decode('ascii'))

            match = _DUMP_LINE_RE.match(line)
            if not match:
                raise exceptions.InvalidResponse(line)

            line_data = match.groupdict()

            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.GlucoseReading(
                date, float(line_data['value']), meal=meal, comment=comment)