Пример #1
0
    def _send_text_command(self, command):
        """Send a command to the device that expects a text reply."""
        self._send_command(self.TEXT_CMD, command)

        # Reply can stretch multiple buffers
        full_content = ''
        while True:
            message_type, content = self._read_response()

            if message_type != self.TEXT_REPLY_CMD:
                raise exceptions.InvalidResponse(
                    'Message type %02x does not match expectations: %s' %
                    (message_type, content.decode('ascii')))

            full_content += content.decode('ascii')

            if _TEXT_COMPLETION_RE.search(full_content):
                break

        match = _TEXT_REPLY_FORMAT.search(full_content)
        if not match:
            raise exceptions.InvalidResponse(full_content)

        message = match.group('message')
        _verify_checksum(message, match.group('checksum'))

        if match.group('status') != 'OK':
            raise exceptions.InvalidResponse(message or "Command failed")

        return message
Пример #2
0
def _parse_result(data):
    """\
  I assume empty meter has final line "Log Empty END"
  and no trailing empty line:
  blank, dev, soft, date, "Log Empty END"
  I assume non-empty meter has lines:
  blank, dev, soft, date, count, blank line, result*count, checksum
  """
    info = _parse_info(data)
    n = info['device_nrresults_']
    if n == 0:
        len_data1 = _INFO_SIZE
    else:
        len_data1 = _INFO_SIZE + n + 2
        if data[_INFO_SIZE] != '':
            logging.debug('_parse_result:  last line not blank:  %r', data)
            raise exceptions.InvalidResponse('\n'.join(data))
    if len(data) != len_data1:
        msg = '_parse_result:  len(data)= %d correct len_data= %d  ' % (
            len(data), len_data1)
        logging.debug(msg + '%r', data)
        raise exceptions.InvalidResponse('\n'.join(data))
    j1 = _INFO_SIZE + 1
    j2 = _INFO_SIZE + 1 + n
    reslog = [_parse_resline(data[j], j) for j in range(j1, j2)]
    if n > 0:
        checksum = _parse_checksum(data[j2])
    return {'info': info, 'reslog': reslog, 'checksum': checksum}
Пример #3
0
    def _read_text_response(self) -> Sequence[bytes]:
        all_lines: List[bytes] = []

        while True:
            line = self._readline()
            if not line.endswith(b"\r\n"):
                raise exceptions.InvalidResponse(
                    f"Corrupted response line: {line!r}")
            all_lines.append(line)

            if line == b"]\r\n":
                break

        if all_lines[0] != b"[\r\n":
            raise exceptions.InvalidResponse(
                f"Unexpected first response line: {all_lines!r}")

        wire_checksum = int(all_lines[-2][:-2], base=16)
        calculated_checksum = _crc8_maxim(b"".join(all_lines[:-2]))

        if wire_checksum != calculated_checksum:
            raise exceptions.InvalidChecksum(wire_checksum,
                                             calculated_checksum)

        return [line[:-2] for line in all_lines[1:-2]]
Пример #4
0
    def _get_meter_info(self) -> Sequence[str]:
        self._send_command(_CMD_GET_INFO)
        get_info_response = list(self._read_text_response())
        if len(get_info_response) != 1:
            raise exceptions.InvalidResponse(
                f"Multiple lines returned, when one expected: {get_info_response!r}"
            )
        info = get_info_response[0].split(b",")
        if len(info) != 5:
            raise exceptions.InvalidResponse(
                f"Incomplete information response received: {get_info_response!r}"
            )

        return [component.decode("ascii") for component in info]
Пример #5
0
    def query_multirecord(self, command: bytes) -> Iterator[List[str]]:
        """Queries for, and returns, "multirecords" results.

        Multirecords are used for querying events, readings, history and similar
        other data out of a FreeStyle device. These are comma-separated values,
        variable-length.

        The validation includes the general HID framing parsing, as well as
        validation of the record count, and of the embedded records checksum.

        Args:
          command: The text command to send to the device for the query.

        Returns:
          A CSV reader object that returns a record for each line in the
          reply buffer.
        """
        message = self.send_text_command(command)
        logging.debug("Received multirecord message:\n%s", message)
        if message == "Log Empty\r\n":
            return iter(())

        match = _MULTIRECORDS_FORMAT.search(message)
        if not match:
            raise exceptions.InvalidResponse(message)

        records_str = match.group("message")
        _verify_checksum(records_str, match.group("checksum"))

        logging.debug("Received multi-record string: %s", records_str)

        return csv.reader(records_str.split("\r\n"))
Пример #6
0
    def _get_multirecord(self, command):
        """Queries for, and returns, "multirecords" results.

        Multirecords are used for querying events, readings, history and similar
        other data out of a FreeStyle device. These are comma-separated values,
        variable-length.

        The validation includes the general HID framing parsing, as well as
        validation of the record count, and of the embedded records checksum.

        Args:
          command: (bytes) the text command to send to the device for the query.

        Returns:
          (csv.reader): a CSV reader object that returns a record for each line
             in the record file.
        """
        message = self._send_text_command(command)
        match = _MULTIRECORDS_FORMAT.search(message)
        if not match:
            raise exceptions.InvalidResponse(message)

        records_str = match.group('message')
        _verify_checksum(records_str, match.group('checksum'))

        logging.debug('Received multi-record string: %s', records_str)

        return csv.reader(records_str.split('\r\n'))
Пример #7
0
    def read_packet(self):
        preamble = self.serial_.read(3)
        if len(preamble) != 3:
            raise exceptione.InvalidResponse(
                response='Expected 3 bytes, received %d' % len(preamble))
        if preamble[0:_IDX_LENGTH] != _RECV_PREAMBLE:
            raise exceptions.InvalidResponse(
                response='Unexpected preamble %r' % pramble[0:_IDX_LENGTH])

        msglen = preamble[_IDX_LENGTH]
        message = self.serial_.read(msglen)
        if len(message) != msglen:
            raise exception.InvalidResponse(
                response='Expected %d bytes, received %d' %
                (msglen, len(message)))
        if message[_IDX_ETX] != _ETX:
            raise exception.InvalidResponse(
                response='Unexpected end-of-transmission byte: %02x' %
                message[_IDX_ETX])

        # Calculate the checksum up until before the checksum itself.
        msgdata = message[:_IDX_CHECKSUM]

        cksum = xor_checksum(msgdata)
        if cksum != message[_IDX_CHECKSUM]:
            raise exception.InvalidChecksum(message[_IDX_CHECKSUM], cksum)

        return msgdata
Пример #8
0
    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
Пример #9
0
def _parse_checksum(line):
    match = _CHECKSUM_RE.match(line)
    if not match:
        raise exceptions.InvalidResponse('\n'.join(line))
    checksum_str = match.group('checksum')
    checksum = int(checksum_str, 16)
    return checksum
Пример #10
0
    def _set_device_datetime(self, date: datetime.datetime) -> datetime.datetime:
        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()
Пример #11
0
    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)
Пример #12
0
    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.
    """
        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)
Пример #13
0
    def get_readings(self) -> Generator[common.AnyReading, None, None]:
        """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)
Пример #14
0
 def _extract_meal(self, record):  # pylint: disable=no-self-use
     if record[_AFTER_MEAL_CSV_KEY] and record[_BEFORE_MEAL_CSV_KEY]:
         raise exceptions.InvalidResponse(
             'Reading cannot be before and after meal.')
     elif record[_AFTER_MEAL_CSV_KEY]:
         return common.Meal.AFTER
     elif record[_BEFORE_MEAL_CSV_KEY]:
         return common.Meal.BEFORE
     else:
         return common.Meal.NONE
Пример #15
0
def _parse_info(data):
    info = {}
    if len(data) < _INFO_SIZE:
        msg = '_parse_info:  len(data)=%d < %d lines:  ' % (len(data),
                                                            _INFO_SIZE)
        logging.debug(msg + '%r', data)
        raise exceptions.InvalidResponse('\n'.join(data))
    if data[0] != '':
        msg = '_parse_info:  first line not blank:  '
        logging.debug(msg + '%r', data)
        raise exceptions.InvalidResponse('\n'.join(data))
    info['device_version_'] = data[1]
    info['software_revision_'] = data[2]
    info['device_serialno_'] = 'N/A'
    info['device_glucose_unit_'] = common.Unit.MG_DL
    # info['device_glucose_unit_']    = common.Unit.MMOL_L
    info['device_current_date_time_'] = _parse_clock(data[3])
    info['device_nrresults_'] = _parse_nrresults(data[4])
    return info
Пример #16
0
    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:]
Пример #17
0
    def _send_text_command(self, command):
        # type: (bytes) -> Text
        """Send a command to the device that expects a text reply."""
        self._send_command(self.TEXT_CMD, command)

        # Reply can stretch multiple buffers
        full_content = b''
        while True:
            message_type, content = self._read_response()

            logging.debug('Received message: type %02x content %s',
                          message_type, content.hex())

            if message_type != self.TEXT_REPLY_CMD:
                raise exceptions.InvalidResponse(
                    'Message type %02x does not match expectations: %r' %
                    (message_type, content))

            full_content += content

            if _TEXT_COMPLETION_RE.search(full_content):
                break

        match = _TEXT_REPLY_FORMAT.search(full_content)
        if not match:
            raise exceptions.InvalidResponse(full_content)

        message = match.group('message')
        _verify_checksum(message, match.group('checksum'))

        if match.group('status') != b'OK':
            raise exceptions.InvalidResponse(message or "Command failed")

        # If there is anything in the response that is not ASCII-safe, this is
        # probably in the patient name. The Windows utility does not seem to
        # validate those, so just replace anything non-ASCII with the correct
        # unknown codepoint.
        return message.decode('ascii', 'replace')
Пример #18
0
    def send_text_command(self, command: bytes) -> str:
        """Send a command to the device that expects a text reply."""
        self.send_command(self._text_message_type, command)

        # Reply can stretch multiple buffers
        full_content = b""
        while True:
            message_type, content = self.read_response()

            logging.debug(
                "Received message: type %02x content %s", message_type, content.hex()
            )

            if message_type != self._text_reply_message_type:
                raise exceptions.InvalidResponse(
                    f"Message type {message_type:02x}: content does not match expectations: {content!r}"
                )

            full_content += content

            if _TEXT_COMPLETION_RE.search(full_content):
                break

        match = _TEXT_REPLY_FORMAT.search(full_content)
        if not match:
            raise exceptions.InvalidResponse(repr(full_content))

        message = match.group("message")
        _verify_checksum(message, match.group("checksum"))

        if match.group("status") != b"OK":
            raise exceptions.InvalidResponse(repr(message) or "Command failed")

        # If there is anything in the response that is not ASCII-safe, this is
        # probably in the patient name. The Windows utility does not seem to
        # validate those, so just replace anything non-ASCII with the correct
        # unknown codepoint.
        return message.decode("ascii", "replace")
Пример #19
0
    def set_datetime(self, date=datetime.datetime.now()):
        setdatecmd = date.strftime('ADATE%Y%m%d%H%M').encode('ascii')

        # Ignore the readings count.
        self.wait_and_ready()

        self.send_message(setdatecmd)
        response = self.read_message()
        if response != _DATE_SET_MESSAGE:
            raise exceptions.InvalidResponse(response=response)

        # The date we return should only include up to minute, unfortunately.
        return datetime.datetime(date.year, date.month, date.day, date.hour,
                                 date.minute)
Пример #20
0
def _parse_nrresults(countstr):
    """Convert the count string used by the device into number of results.

  Args:
    countstr:  a string as returned by the device during information handling.
    Special case:  no results ('Log Empty END') returns 0.
  """
    match_empty = _EMPTY_RE.match(countstr)
    match_count = _COUNT_RE.match(countstr)
    if match_empty:
        return 0
    elif match_count:
        return int(countstr)
    else:
        raise exceptions.InvalidResponse(countstr)
Пример #21
0
    def get_datetime(self) -> datetime.datetime:
        """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))
Пример #22
0
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'))
    time = match.group('time')
    hour, minute, second = map(int, time.split(':'))
    return datetime.datetime(year, month, day, hour, minute, second)
Пример #23
0
    def _send_command(self, message_type, command):
        """Send a raw command to the device.

        Args:
          message_type: (int) The first byte sent with the report to the device.
          command: (bytes) The command to send out the device.
        """
        cmdlen = len(command)
        assert cmdlen <= 62

        # First byte in the written buffer is the report number, on Linux HID
        # interface.
        usb_packet = b'\x00' + _STRUCT_PREAMBLE.pack(
            message_type, cmdlen) + command + bytes(62 - cmdlen)

        if self.handle_.write(usb_packet) < 0:
            raise exceptions.InvalidResponse()
Пример #24
0
    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()
Пример #25
0
    def _set_device_datetime(self,
                             date: datetime.datetime) -> datetime.datetime:
        datetime_representation = date.strftime("%y%m%d%H%M").encode("ascii")
        command_string = b"[\r\n" + datetime_representation + b"\r\n"

        checksum = _crc8_maxim(command_string)
        assert 0 <= checksum <= 255

        command_string += f"{checksum:02X}".encode("ascii") + b"\r\n]\r\n"

        command = _CMD_SET_DATETIME + command_string
        self._send_command(command)
        response = self.serial_.read()
        if response == b"P":
            return date
        else:
            raise exceptions.InvalidResponse(
                f"Unexpected response {response!r}.")
Пример #26
0
def _parse_clock(datestr: str) -> datetime.datetime:
    """Convert the date/time string used by 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(x) for x in match.group("time").split(":"))

    return datetime.datetime(year, month, day, hour, minute, second)
Пример #27
0
    def _send_command(
        self,
        command: int,
        message: bytes = _EMPTY_MESSAGE,
        validate_response: bool = True,
    ) -> Tuple[int, bytes]:
        pkt = _make_packet(command, message)
        logging.debug("sending packet: %s", binascii.hexlify(pkt))

        self.serial_.write(pkt)
        self.serial_.flush()
        response = self.buffered_reader_.parse_stream(self.serial_)
        logging.debug("received packet: %r", response)

        if validate_response and response.data.value.command != command:
            raise exceptions.InvalidResponse(response)

        return response.data.value.command, response.data.value.message
Пример #28
0
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)
Пример #29
0
def _parse_resline(line, j):
    match = _READING_RE.match(line)
    if not match:
        raise exceptions.InvalidResponse('error line %d = %s' % (j, line))
    READING = match.group('reading')
    MONTH = match.group('month')
    DAY = match.group('day')
    YEAR = match.group('year')
    TIME = match.group('time')
    TYPE = match.group('type')
    SENTINEL = match.group('sentinel')
    if TYPE != '00':
        logging.warning('TYPE == %s  NORMALLY type == 00' % TYPE)
    if READING == 'HI ':
        value = float("inf")
    else:
        value = float(READING)
    month = _MONTH_MATCHES[MONTH]
    day = int(DAY)
    year = int(YEAR)
    hour, minute = map(int, 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.
    # fixme
    return {
        'value': value,
        'day': day,
        'month': month,
        'year': year,
        'hour': hour,
        'minute': minute,
        'timestamp': timestamp,
        'READING': READING,
        'TYPE': TYPE,
        'SENTINEL': SENTINEL
    }
Пример #30
0
    def get_readings(self) -> Generator[common.AnyReading, None, 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.

        """
        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))

        # Extract the checksum from the last line.
        checksum_match = _CHECKSUM_RE.match(data[-1])
        if not checksum_match:
            raise exceptions.InvalidResponse("\n".join(data))

        expected_checksum = int(checksum_match.group("checksum"), 16)
        # exclude the last line in the checksum calculation, as that's the
        # checksum itself. The final \r\n is added separately.
        calculated_checksum = sum(ord(c) for c in "\r\n".join(data[:-1])) + 0xD + 0xA

        if expected_checksum != calculated_checksum:
            raise exceptions.InvalidChecksum(expected_checksum, calculated_checksum)

        for line in data[5:-1]:
            match = _READING_RE.match(line)
            if not match:
                raise exceptions.InvalidResponse(line)

            if match.group("type") != "G":
                logging.warning("Non-glucose readings are not supported, ignoring.")
                continue

            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 = map(int, 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.GlucoseReading(timestamp, value)