From 475fceae1d436bd6ecdc9aa465e204dec94c0fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 21:06:51 +0000 Subject: [PATCH 01/58] Move the binary script to glucometerutils/ and create a starting shim. The shim needs to be renamed, but this makes the history simpler to read. --- glucometer | 7 +++++++ glucometer.py => glucometerutils/glucometer.py | 3 --- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100755 glucometer rename glucometer.py => glucometerutils/glucometer.py (99%) diff --git a/glucometer b/glucometer new file mode 100755 index 0000000..3b8bf70 --- /dev/null +++ b/glucometer @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# -*- python -*- + +from glucometerutils import glucometer + +if __name__ == "__main__": + glucometer.main() diff --git a/glucometer.py b/glucometerutils/glucometer.py similarity index 99% rename from glucometer.py rename to glucometerutils/glucometer.py index abed647..cd2331c 100755 --- a/glucometer.py +++ b/glucometerutils/glucometer.py @@ -145,6 +145,3 @@ def main(): return 1 device.disconnect() - -if __name__ == "__main__": - main() From 26021fd433d504237a65c6ab50601fd7d7484ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 21:07:38 +0000 Subject: [PATCH 02/58] Rename the shim to match the old name. --- glucometer => glucometer.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename glucometer => glucometer.py (100%) diff --git a/glucometer b/glucometer.py similarity index 100% rename from glucometer rename to glucometer.py From 613b2d7c31d51cc143de53812ee63996a2fb18c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 21:10:01 +0000 Subject: [PATCH 03/58] Rewrite setup.py to use setuptools. This should make it easier to add dependencies and so on. --- setup.py | 55 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/setup.py b/setup.py index 82a9d07..247a48b 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,38 @@ # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup, find_packages setup( - name = 'glucometerutils', - packages = ['glucometerutils', 'glucometerutils.drivers', 'glucometerutils.support'], - scripts = ['glucometer.py'], - version = '1', - description = 'Glucometer access utilities', - author = 'Diego Elio Pettenò', - author_email = 'flameeyes@flameeyes.eu', - url = 'https://www.flameeyes.eu/p/glucometerutils', - download_url = 'https://www.flameeyes.eu/files/glucometerutils.tgz', - keywords = ['glucometer', 'diabetes'], - python_requires = '~=3.4', - classifiers = [ - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: End Users/Desktop', - 'Topic :: Scientific/Engineering :: Medical Science Apps.', - ], + name = 'glucometerutils', + version = '1', + description = 'Glucometer access utilities', + author = 'Diego Elio Pettenò', + author_email = 'flameeyes@flameeyes.eu', + url = 'https://www.flameeyes.eu/p/glucometerutils', + download_url = 'https://www.flameeyes.eu/files/glucometerutils.tgz', + keywords = ['glucometer', 'diabetes'], + python_requires = '~=3.4', + classifiers = [ + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: End Users/Desktop', + 'Topic :: Scientific/Engineering :: Medical Science Apps.', + ], + packages = find_packages( + exclude=['test', 'udev']), + data_files = [ + ('lib/udev/rules', ['udev/69-glucometerutils.rules']), + ], + extras_require = { + 'test': ['abseil-py'], + }, + entry_points = { + 'console_scripts': [ + 'glucometer=glucometerutils.glucometer:main' + ] + }, ) From b8aa129750be9a6a56f14987401010edc8514b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 21:35:33 +0000 Subject: [PATCH 04/58] Add driver dependencies to setup.py, and document how to install this. This should address Issue 5 (theoretically some of the dependencies are OS-specific but that's a longer problem). Also partially addresses Issue 9 because now we have an easy to understand "install and try out" option. --- README | 14 ++++++++++++++ setup.py | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/README b/README index d87f3b8..a070a05 100644 --- a/README +++ b/README @@ -13,6 +13,20 @@ follows: * `datetime` reads or updates the date and time of the device clock. * `zero` deletes all the recorded readings (only implemented for few devices). +## Example Usage + +Most of the drivers require optional dependencies, and those are listed in the +table below. If you do not want to install the dependencies manually, you should +be able to set this up using `virtualenv` and `pip`: + +```shell +$ python3 -m venv $(pwd)/glucometerutils-venv +$ . glucometerutils-venv/bin/activate +(glucometerutils-venv) $ DRIVER=myglucometer-driver # see table below +(glucometerutils-venv) $ pip install git+https://github.com/Flameeyes/glucometerutils.git#egg=project[${DRIVER}] +(glucometerutils-venv) $ glucometer --driver ${DRIVER} help +``` + ## Supported devices Please see the following table for the driver for each device that is known and diff --git a/setup.py b/setup.py index 247a48b..2577ca0 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,17 @@ ], extras_require = { 'test': ['abseil-py'], + # These are all the drivers' dependencies. Optional dependencies are + # listed as mandatory for the feature. + 'otultra2': ['pyserial'], + 'otultraeasy': ['pyserial'], + 'otverio2015': ['python-scsi'], + 'fsinsulinx': ['hidapi'], + 'fslibre': ['hidapi'], + 'fsoptium': ['hidapi'], + 'fsprecisionneo': ['hidapi'], + 'accucheck_reports': [], + 'sdcodefree': ['pyserial'], }, entry_points = { 'console_scripts': [ From fb49b60757c2a3367b8aa4b69299b740710bb4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 21:45:05 +0000 Subject: [PATCH 05/58] deps: correct dependency for fsoptium. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2577ca0..5da8086 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ 'otverio2015': ['python-scsi'], 'fsinsulinx': ['hidapi'], 'fslibre': ['hidapi'], - 'fsoptium': ['hidapi'], + 'fsoptium': ['pyserial'], 'fsprecisionneo': ['hidapi'], 'accucheck_reports': [], 'sdcodefree': ['pyserial'], From 09a5fe1f8d34ee9a9f8263624a9b92a2bcd96f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 28 Dec 2017 20:16:22 +0000 Subject: [PATCH 06/58] freestyle: replace the custom struct and bytemangling with construct. This simplifies the code and enforces the validation within the format documentation too. construct can handle the full package verification, including dealing with padding. --- README | 31 ++++++++++++++-------------- glucometerutils/support/freestyle.py | 19 ++++++++++------- setup.py | 6 +++--- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/README b/README index a070a05..1f42487 100644 --- a/README +++ b/README @@ -32,21 +32,21 @@ $ . glucometerutils-venv/bin/activate Please see the following table for the driver for each device that is known and supported. -| Manufacturer | Model Name | Driver | Dependencies | -| --- | --- | --- | --- | -| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | -| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | -| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [hidapi]‡ | -| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [hidapi]‡ | -| Roche | Accu-Chek Mobile | `accuchek_reports` | | -| SD Biosensor | SD CodeFree | `sdcodefree` | [pyserial] | +| Manufacturer | Model Name | Driver | Dependencies | +| --- | --- | --- | --- | +| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | +| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [pyserial] | +| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | +| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | +| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | +| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | +| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | +| Roche | Accu-Chek Mobile | `accuchek_reports` | | +| SD Biosensor | SD CodeFree | `sdcodefree` | [pyserial] | † Untested. ‡ Optional dependency on Linux; required on other operating systems. @@ -60,6 +60,7 @@ If you have knowledge of a protocol of a glucometer you would have supported, please provide a reference, possibly by writing a specification and contribute it to https://github.com/Flameeyes/glucometer-protocols/. +[construct]: https://construct.readthedocs.io/en/latest/ [pyserial]: https://pythonhosted.org/pyserial/ [python-scsi]: https://github.com/rosjat/python-scsi [hidapi]: https://pypi.python.org/pypi/hidapi diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py index d722c35..7610729 100644 --- a/glucometerutils/support/freestyle.py +++ b/glucometerutils/support/freestyle.py @@ -15,7 +15,8 @@ import datetime import logging import re -import struct + +import construct from glucometerutils import exceptions from glucometerutils.support import hiddevice @@ -24,7 +25,13 @@ # protocol. _INIT_SEQUENCE = (0x04, 0x05, 0x15, 0x01) -_STRUCT_PREAMBLE = struct.Struct(' Date: Thu, 28 Dec 2017 22:05:27 +0000 Subject: [PATCH 07/58] Fix dependency name for absl-py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5e65e1e..d7cc51f 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ ('lib/udev/rules', ['udev/69-glucometerutils.rules']), ], extras_require = { - 'test': ['abseil-py'], + 'test': ['absl-py'], # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], From 95461d5875bae907b53b4be08b49fece5ee94677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Fri, 29 Dec 2017 20:51:28 +0000 Subject: [PATCH 08/58] Fix error in meter info output, after conversion of Unit to enum. --- glucometerutils/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/common.py b/glucometerutils/common.py index 318fc86..ba4f018 100644 --- a/glucometerutils/common.py +++ b/glucometerutils/common.py @@ -149,4 +149,4 @@ def __str__(self): Native Unit: {native_unit} """).format(model=self.model, serial_number=self.serial_number, version_information_string=version_information_string, - native_unit=self.native_unit) + native_unit=self.native_unit.value) From 18321782bba3986d4ce55e9559026aca1bdbf0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Fri, 29 Dec 2017 20:51:56 +0000 Subject: [PATCH 09/58] Add more tests to cover the basic translation of objects to strings. This would have found an extra bug that was pushed unfixed after enum conversion, and two bugs that I did find during the conversion. --- test/test_common.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/test_common.py b/test/test_common.py index 92805a5..8cff341 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -6,6 +6,7 @@ __copyright__ = 'Copyright © 2013, Diego Elio Pettenò' __license__ = 'MIT' +import datetime import os import sys import unittest @@ -52,5 +53,78 @@ def test_invalid_values(self, from_unit, to_unit): common.convert_glucose_unit(100, from_unit, to_unit) +class TestGlucoseReading(parameterized.TestCase): + + TEST_DATETIME = datetime.datetime(2018, 1 ,1, 0, 30, 45) + + def test_minimal(self): + reading = common.GlucoseReading(self.TEST_DATETIME, 100) + self.assertEqual(reading.as_csv(common.Unit.MG_DL), + '"2018-01-01 00:30:45","100.00","","blood sample",""') + + @parameterized.named_parameters( + ('_mgdl', common.Unit.MG_DL, 100), + ('_mmoll', common.Unit.MMOL_L, 5.56)) + def test_value(self, unit, expected_value): + reading = common.GlucoseReading(self.TEST_DATETIME, 100) + self.assertAlmostEqual( + reading.get_value_as(unit), expected_value, places=2) + + @parameterized.named_parameters( + ('_meal_none', + {'meal': common.Meal.NONE}, + '"2018-01-01 00:30:45","100.00","","blood sample",""'), + ('_meal_before', + {'meal': common.Meal.BEFORE}, + '"2018-01-01 00:30:45","100.00","Before Meal","blood sample",""'), + ('_meal_after', + {'meal': common.Meal.AFTER}, + '"2018-01-01 00:30:45","100.00","After Meal","blood sample",""'), + ('_measurement_blood', + {'measure_method': common.MeasurementMethod.BLOOD_SAMPLE}, + '"2018-01-01 00:30:45","100.00","","blood sample",""'), + ('_measurement_cgm', + {'measure_method': common.MeasurementMethod.CGM}, + '"2018-01-01 00:30:45","100.00","","CGM",""'), + ('_comment', + {'comment': 'too much'}, + '"2018-01-01 00:30:45","100.00","","blood sample","too much"'), + ('_comment_quoted', + {'comment': '"too" much'}, + '"2018-01-01 00:30:45","100.00","","blood sample","\"too\" much"'), + ) + def test_csv(self, kwargs_dict, expected_csv): + reading = common.GlucoseReading( + self.TEST_DATETIME, 100, **kwargs_dict) + self.assertEqual(reading.as_csv(common.Unit.MG_DL), expected_csv) + + +class TestMeterInfo(parameterized.TestCase): + + @parameterized.named_parameters( + ('_no_serial_number', + {}, + 'Serial Number: N/A\n'), + ('_serial_number', + {'serial_number': 1234}, + 'Serial Number: 1234\n'), + ('_no_version_information', + {}, + 'Version Information:\n N/A\n'), + ('_version_information_1', + {'version_info': ['test']}, + 'Version Information:\n test\n'), + ('_version_information_2', + {'version_info': ['test', 'test2']}, + 'Version Information:\n test\n test2\n'), + ('_default_native_unit', + {}, + 'Native Unit: mg/dL\n'), + ) + def test_meter_info(self, kwargs_dict, expected_fragment): + info = common.MeterInfo(self.id(), **kwargs_dict) + self.assertIn(expected_fragment, str(info)) + + if __name__ == '__main__': unittest.main() From e9c3a20e099ee44d0873c1b73cb4300861cc9003 Mon Sep 17 00:00:00 2001 From: Muhammad Kaisar Arkhan Date: Sat, 30 Dec 2017 10:45:05 +0700 Subject: [PATCH 10/58] Use pytest to run tests Closes https://github.com/Flameeyes/glucometerutils/issues/34 --- .gitignore | 1 + setup.cfg | 15 +++++++++++++++ setup.py | 22 +++++++++++++++++++++- test-requirements.txt | 4 ++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 setup.cfg create mode 100644 test-requirements.txt diff --git a/.gitignore b/.gitignore index 894b10a..70028ed 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /MANIFEST /dist/ __pycache__/ +.cache diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cd71828 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[tool:pytest] +addopts = + --color=yes + --ignore=setup.py + --ignore=test-requirements.txt + -r a +norecursedirs = + .git + dist + build + venv + .env +testpaths = + test +timeout = 120 diff --git a/setup.py b/setup.py index d7cc51f..c49745a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,23 @@ # -*- coding: utf-8 -*- +import sys + from setuptools import setup, find_packages +from setuptools.command.test import test as TestCommand + + +with open('test-requirements.txt') as requirements: + test_required = requirements.read().splitlines() + + +class PyTestCommand(TestCommand): + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import pytest + errno = pytest.main([]) + sys.exit(errno) + setup( name = 'glucometerutils', @@ -27,8 +44,8 @@ data_files = [ ('lib/udev/rules', ['udev/69-glucometerutils.rules']), ], + tests_require = test_required, extras_require = { - 'test': ['absl-py'], # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], @@ -46,4 +63,7 @@ 'glucometer=glucometerutils.glucometer:main' ] }, + cmdclass = { + 'test': PyTestCommand, + }, ) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..9f7c85b --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +absl-py +pytest +pytest-timeout +pyserial From 04d69b610f8955145dba328f74e5d7b756a49d16 Mon Sep 17 00:00:00 2001 From: Muhammad Kaisar Arkhan Date: Sat, 30 Dec 2017 10:51:40 +0700 Subject: [PATCH 11/58] Run test and build on Travis CI Closes https://github.com/Flameeyes/glucometerutils/issues/35 --- .gitignore | 2 ++ .travis.yml | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 .travis.yml diff --git a/.gitignore b/.gitignore index 70028ed..90d1a19 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /dist/ __pycache__/ .cache +build +*.egg-info/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1948c7f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 3.4 + - 3.5 + - 3.6 + +install: + - pip install -r test-requirements.txt + +script: + - py.test + - python setup.py bdist_wheel + - pip install ./dist/glucometerutils-*.whl From 222076484b6b5533d106df849f3bb0d37ad40afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 30 Dec 2017 15:17:51 +0000 Subject: [PATCH 12/58] freestyle: rename miscopied constant name. --- glucometerutils/support/freestyle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py index 7610729..50f5319 100644 --- a/glucometerutils/support/freestyle.py +++ b/glucometerutils/support/freestyle.py @@ -25,7 +25,7 @@ # protocol. _INIT_SEQUENCE = (0x04, 0x05, 0x15, 0x01) -_LIFESCAN_MESSAGE = construct.Struct( +_FREESTYLE_MESSAGE = construct.Struct( 'hid_report' / construct.Const(construct.Byte, 0), 'message_type' / construct.Byte, 'command' / construct.Padded( @@ -100,7 +100,7 @@ def _send_command(self, message_type, command): message_type: (int) The first byte sent with the report to the device. command: (bytes) The command to send out the device. """ - usb_packet = _LIFESCAN_MESSAGE.build( + usb_packet = _FREESTYLE_MESSAGE.build( {'message_type': message_type, 'command': command}) self._write(usb_packet) From fc7e86d6d5f9c8df63d1a09c28f743b29b024d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 31 Dec 2017 21:35:57 +0000 Subject: [PATCH 13/58] serial devices: disable xonxoff by default. With 2a825fb889735fa881566d1764cc48d2814447d2 the parameters to open the serial device were lifted from the fsoptium driver, which was the only one passing xonxoff=True. The Optium device has no problem with disabling this feature, but the codefree driver hangs if this is set to True. So instead set it to False and get rid of it. --- glucometerutils/support/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/support/serial.py b/glucometerutils/support/serial.py index d4f352b..ce4ac20 100644 --- a/glucometerutils/support/serial.py +++ b/glucometerutils/support/serial.py @@ -64,4 +64,4 @@ def __init__(self, device): writeTimeout=None, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, - xonxoff=True, rtscts=False, dsrdtr=False) + xonxoff=False, rtscts=False, dsrdtr=False) From 4ec3a8e678d4a3889921138fe83970aba39992f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 31 Dec 2017 21:48:49 +0000 Subject: [PATCH 14/58] sdcodefree: rewrite using construct and improve readability. This replaces the use of manual structures with well-defined construct entries; it also give consistency to packet vs message. Log input and output messages, to be clearer. --- README | 30 ++--- glucometerutils/drivers/sdcodefree.py | 165 ++++++++++++-------------- setup.py | 2 +- 3 files changed, 94 insertions(+), 103 deletions(-) diff --git a/README b/README index 1f42487..a5dcd32 100644 --- a/README +++ b/README @@ -32,21 +32,21 @@ $ . glucometerutils-venv/bin/activate Please see the following table for the driver for each device that is known and supported. -| Manufacturer | Model Name | Driver | Dependencies | -| --- | --- | --- | --- | -| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | -| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | -| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | -| Roche | Accu-Chek Mobile | `accuchek_reports` | | -| SD Biosensor | SD CodeFree | `sdcodefree` | [pyserial] | +| Manufacturer | Model Name | Driver | Dependencies | +| --- | --- | --- | --- | +| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | +| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [pyserial] | +| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | +| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | +| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | +| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | +| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | +| Roche | Accu-Chek Mobile | `accuchek_reports` | | +| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | † Untested. ‡ Optional dependency on Linux; required on other operating systems. diff --git a/glucometerutils/drivers/sdcodefree.py b/glucometerutils/drivers/sdcodefree.py index 4a375bd..2d145cc 100644 --- a/glucometerutils/drivers/sdcodefree.py +++ b/glucometerutils/drivers/sdcodefree.py @@ -19,96 +19,83 @@ __copyright__ = 'Copyright © 2017, Diego Elio Pettenò' __license__ = 'MIT' -import array -import collections +import binascii import datetime +import enum import functools import logging import operator -import struct -import time + +import construct from glucometerutils import common from glucometerutils import exceptions from glucometerutils.support import serial -_STX = 0x53 # Not really 'STX' -_ETX = 0xAA # Not really 'ETX' - -_DIR_IN = 0x20 -_DIR_OUT = 0x10 - -_IDX_STX = 0 -_IDX_DIRECTION = 1 -_IDX_LENGTH = 2 -_IDX_CHECKSUM = -2 -_IDX_ETX = -1 +def xor_checksum(msg): + return functools.reduce(operator.xor, msg) -_RECV_PREAMBLE = b'\x53\x20' +class Direction(enum.Enum): + In = 0x20 + Out = 0x10 + +_PACKET = construct.Struct( + 'stx' / construct.Const(construct.Byte, 0x53), + 'direction' / construct.SymmetricMapping( + construct.Byte, + {e: e.value for e in Direction}), + 'length' / construct.Rebuild( + construct.Byte, lambda ctx: len(ctx.message) + 2), + 'message' / construct.Bytes(length=lambda ctx: ctx.length - 2), + 'checksum' / construct.Checksum( + construct.Byte, xor_checksum, construct.this.message), + 'etx' / construct.Const(construct.Byte, 0xAA) +) + +_FIRST_MESSAGE = construct.Struct( + construct.Const(construct.Byte, 0x30), + 'count' / construct.Int16ub, + construct.Const(construct.Byte, 0xAA)[19]) _CHALLENGE_PACKET_FULL = b'\x53\x20\x04\x10\x30\x20\xAA' -_RESPONSE_PACKET = b'\x10\x40' - -_DATE_SET_PACKET = b'\x10\x10' +_RESPONSE_MESSAGE = b'\x10\x40' -_DISCONNECT_PACKET = b'\x10\x60' -_DISCONNECTED_PACKET = b'\x10\x70' +_DATE_SET_MESSAGE = b'\x10\x10' -_STRUCT_READINGS_COUNT = struct.Struct('>H') +_DISCONNECT_MESSAGE = b'\x10\x60' +_DISCONNECTED_MESSAGE = b'\x10\x70' -_FETCH_PACKET = b'\x10\x60' - -_ReadingRecord = collections.namedtuple( - '_ReadingRecord', - ('unknown1', 'unknown2', 'year', 'month', 'day', 'hour', 'minute', - 'value', 'meal_flag')) -_STRUCT_READING = struct.Struct('>BBBBBBBHB') +_FETCH_MESSAGE = b'\x10\x60' _MEAL_FLAG = { - 0x00: common.Meal.NONE, - 0x10: common.Meal.BEFORE, - 0x20: common.Meal.AFTER, + common.Meal.NONE: 0x00, + common.Meal.BEFORE: 0x10, + common.Meal.AFTER: 0x20, } -def parse_reading(msgdata): - return _ReadingRecord(*_STRUCT_READING.unpack_from(msgdata)) +_READING = construct.Struct( + construct.Byte[2], + 'year' / construct.Byte, + 'month' / construct.Byte, + 'day' / construct.Byte, + 'hour' / construct.Byte, + 'minute' / construct.Byte, + 'value' / construct.Int16ub, + 'meal' / construct.SymmetricMapping( + construct.Byte, _MEAL_FLAG), + construct.Byte[7], +) -def xor_checksum(msg): - return functools.reduce(operator.xor, msg) class Device(serial.SerialDevice): BAUDRATE = 38400 DEFAULT_CABLE_ID = '10c4:ea60' # Generic cable. TIMEOUT = 300 # We need to wait for data from the device. - 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 + def read_message(self): + pkt = _PACKET.parse_stream(self.serial_) + logging.debug('received packet: %r', pkt) + return pkt.message def wait_and_ready(self): challenge = self.serial_.read(1) @@ -116,6 +103,7 @@ def wait_and_ready(self): # The first packet read may have a prefixed zero, it might be a bug in # the cp210x driver or device, but discard it if found. if challenge == b'\0': + logging.debug('spurious null byte received') challege = self.serial_.read(1) if challenge != b'\x53': raise exceptions.ConnectionFailed( @@ -127,30 +115,33 @@ def wait_and_ready(self): raise exceptions.ConnectionFailed( message='Unexpected challenge %r' % challenge) - self.send_packet(_RESPONSE_PACKET) + logging.debug( + 'challenge packet received: %s', binascii.hexlify(challenge)) + + self.send_message(_RESPONSE_MESSAGE) # The first packet only contains the counter of how many readings are # available. - first_packet = self.read_packet() - - count = _STRUCT_READINGS_COUNT.unpack_from(first_packet, 1) + first_message = _FIRST_MESSAGE.parse(self.read_message()) + logging.debug('received first message: %r', first_message) - return count[0] + return first_message.count - def send_packet(self, msgdata): - packet = array.array('B') - packet.extend((_STX, _DIR_OUT, len(msgdata)+2)) - packet.extend(msgdata) - packet.extend((xor_checksum(msgdata), _ETX)) - self.serial_.write(packet.tobytes()) + def send_message(self, message): + pkt = _PACKET.build({ + 'message': message, + 'direction': Direction.Out + }) + logging.debug('sending packet: %s', binascii.hexlify(pkt)) + self.serial_.write(pkt) def connect(self): print("Please connect and turn on the device.") def disconnect(self): - self.send_packet(_DISCONNECT_PACKET) - response = self.read_packet() - if response != _DISCONNECTED_PACKET: + self.send_message(_DISCONNECT_MESSAGE) + response = self.read_message() + if response != _DISCONNECTED_MESSAGE: raise exceptions.InvalidResponse(response=response) def get_meter_info(self): @@ -175,9 +166,9 @@ def set_datetime(self, date=datetime.datetime.now()): # Ignore the readings count. self.wait_and_ready() - self.send_packet(setdatecmd) - response = self.read_packet() - if response != _DATE_SET_PACKET: + 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. @@ -185,19 +176,19 @@ def set_datetime(self, date=datetime.datetime.now()): date.hour, date.minute) def zero_log(self): - raise NotmplementedError + raise NotImplementedError def get_readings(self): count = self.wait_and_ready() for _ in range(count): - self.send_packet(_FETCH_PACKET) - rpkt = self.read_packet() + self.send_message(_FETCH_MESSAGE) + message = self.read_message() - r = parse_reading(rpkt) - meal = _MEAL_FLAG[r.meal_flag] + r = _READING.parse(message) + logging.debug('received reading: %r', r) yield common.GlucoseReading( datetime.datetime( 2000 + r.year, r.month, r.day, r.hour, r.minute), - r.value, meal=meal) + r.value, meal=r.meal) diff --git a/setup.py b/setup.py index c49745a..a9b7c18 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def run_tests(self): 'fsoptium': ['pyserial'], 'fsprecisionneo': ['construct', 'hidapi'], 'accucheck_reports': [], - 'sdcodefree': ['pyserial'], + 'sdcodefree': ['construct', 'pyserial'], }, entry_points = { 'console_scripts': [ From 0af0315dba3590ff5a975783cf8f7bc13460a5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 01:16:29 +0000 Subject: [PATCH 15/58] otultraeasy: rewrite using construct for parsing. This removes the wholly complicated _Packet() object and replace it with more readable construct. Unfortunately this appears to reduce performance because of the serial stream buffering, needed to calculate the checksum. It's unfortunate, but it at least avoids a significant amount of custom code. --- README | 4 +- glucometerutils/drivers/otultraeasy.py | 455 +++++++++++-------------- setup.py | 2 +- test/test_otultraeasy.py | 17 - 4 files changed, 201 insertions(+), 277 deletions(-) diff --git a/README b/README index a5dcd32..947348c 100644 --- a/README +++ b/README @@ -35,8 +35,8 @@ supported. | Manufacturer | Model Name | Driver | Dependencies | | --- | --- | --- | --- | | LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | -| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | +| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | | LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | | LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | | Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 0538dac..579c07c 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -18,305 +18,246 @@ __copyright__ = 'Copyright © 2014-2017, Diego Elio Pettenò' __license__ = 'MIT' -import array +import binascii import datetime import logging -import re -import struct -import time + +import construct from glucometerutils import common -from glucometerutils import exceptions from glucometerutils.support import lifescan from glucometerutils.support import serial -_STX = 0x02 -_ETX = 0x03 - -_IDX_STX = 0 -_IDX_LENGTH = 1 -_IDX_CONTROL = 2 -_IDX_DATA = 3 -_IDX_ETX = -3 -_IDX_CHECKSUM = -2 - -_BIT_SENT_COUNTER = 0x01 -_BIT_EXPECT_RECEIVE = 0x02 -_BIT_ACK = 0x04 -_BIT_DISCONNECT = 0x08 -_BIT_MORE = 0x10 - -_READ_SERIAL_NUMBER = b'\x05\x0B\x02\x00\x00\x00\x00\x84\x6A\xE8\x73\x00' -_READ_VERSION = b'\x05\x0D\x02' -_READ_GLUCOSE_UNIT = b'\x05\x09\x02\x09\x00\x00\x00\x00' -_DELETE_RECORDS = b'\x05\x1A' -_READ_DATETIME = b'\x05\x20\x02\x00\x00\x00\x00' -_WRITE_DATETIME = b'\x05\x20\x01' -_READ_RECORD = b'\x05\x1F' _INVALID_RECORD = 501 -_STRUCT_TIMESTAMP = struct.Struct(' 6: - self.cmd.extend(serial.read(self.length - 6)) - - self.cmd.extend(serial.read(3)) - - if self.cmd[_IDX_ETX] != _ETX: - raise lifescan.MalformedCommand( - 'at position %s expected %02x, received %02x' % ( - _IDX_ETX, _ETX, self.cmd[_IDX_ETX])) - - def build_command(self, cmd_bytes): - self.cmd.append(_STX) - self.cmd.append(6 + len(cmd_bytes)) - self.cmd.append(0x00) # link control - self.cmd.extend(cmd_bytes) - self.cmd.extend([_ETX, 0x00, 0x00]) - - @property - def length(self): - if not self.cmd: - return None - - return self.cmd[_IDX_LENGTH] - - def __is_in_control(self, bitmask): - if not self.cmd: - return None - - return bool(self.cmd[_IDX_CONTROL] & bitmask) - - def __set_in_control(self, bitmask, value): - if not self.cmd: - return None - - if value: - self.cmd[_IDX_CONTROL] |= bitmask - else: - self.cmd[_IDX_CONTROL] &= (~bitmask) & 0xFF - - return value - - @property - def sent_counter(self): - return self.__is_in_control(_BIT_SENT_COUNTER) - - @sent_counter.setter - def sent_counter(self, value): - self.__set_in_control(_BIT_SENT_COUNTER, value) - - @property - def expect_receive(self): - return self.__is_in_control(_BIT_EXPECT_RECEIVE) - - @expect_receive.setter - def expect_receive(self, value): - self.__set_in_control(_BIT_EXPECT_RECEIVE, value) - - @property - def checksum(self): - return lifescan.crc_ccitt(self.cmd[:_IDX_CHECKSUM].tobytes()) - - @property - def acknowledge(self): - return self.__is_in_control(_BIT_ACK) - - @acknowledge.setter - def acknowledge(self, value): - self.__set_in_control(_BIT_ACK, value) - - @property - def disconnect(self): - return self.__is_in_control(_BIT_DISCONNECT) - - @disconnect.setter - def disconnect(self, value): - self.__set_in_control(_BIT_DISCONNECT, value) - - @property - def more(self): - return self.__is_in_control(_BIT_MORE) - - @more.setter - def more(self, value): - self.__set_in_control(_BIT_MORE, value) - - def validate_checksum(self): - expected_checksum = self.checksum - received_checksum = self._STRUCT.unpack(self.cmd[_IDX_CHECKSUM:])[0] - if received_checksum != expected_checksum: - raise exceptions.InvalidChecksum(expected_checksum, received_checksum) - - def update_checksum(self): - self._STRUCT.pack_into(self.cmd, _IDX_CHECKSUM, self.checksum) - - def tobytes(self): - return self.cmd.tobytes() - - @property - def data(self): - return self.cmd[_IDX_DATA:_IDX_ETX] - +_EPOCH = datetime.datetime.utcfromtimestamp(0) + +def datetime_to_timestamp(date): + delta = date - _EPOCH + return int(delta.total_seconds()) + + +_PACKET = construct.Struct( + construct.RawCopy( + construct.Embedded( + construct.Struct( + construct.Const(b'\x02'), # stx + 'length' / construct.Rebuild( + construct.Byte, lambda ctx: len(ctx.message) + 6), + construct.EmbeddedBitStruct( + construct.Padding(3), + 'more' / construct.Default(construct.Flag, False), + 'disconnect' / construct.Flag, + 'acknowledge' / construct.Flag, + 'expect_receive' / construct.Flag, + 'sequence_number' / construct.Flag, + ), + 'message' / construct.Bytes(length=lambda ctx: ctx.length - 6), + construct.Const(b'\x03'), # etx + ), + ), + ), + 'checksum' / construct.Checksum( + construct.Int16ul, lifescan.crc_ccitt, construct.this.data), +) + +_COMMAND_SUCCESS = construct.Const(b'\x05\x06') +_TIMESTAMP_ADAPTER = construct.ExprAdapter( + construct.Int32ul, + encoder=lambda obj, ctx: datetime_to_timestamp(obj), + decoder=lambda obj, ctx: datetime.datetime.fromtimestamp(obj)) + +_VERSION_REQUEST = construct.Const(b'\x05\x0d\x02') + +_VERSION_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'version' / construct.PascalString(construct.Byte, encoding='ascii'), +) + +_SERIAL_NUMBER_REQUEST = construct.Const( + b'\x05\x0B\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00') + +_SERIAL_NUMBER_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'serial_number' / construct.GreedyString(encoding='ascii'), +) + +_DATETIME_REQUEST = construct.Struct( + construct.Const(b'\x05\x20'), # 0x20 is the datetime + 'request_type' / construct.Enum(construct.Byte, write=0x01, read=0x02), + 'timestamp' / construct.Default(_TIMESTAMP_ADAPTER, _EPOCH), +) + +_DATETIME_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'timestamp' / _TIMESTAMP_ADAPTER, +) + +_GLUCOSE_UNIT_REQUEST = construct.Const( + b'\x05\x09\x02\x09\x00\x00\x00\x00') + +_GLUCOSE_MAPPING = { + common.Unit.MG_DL: 0x00, + common.Unit.MMOL_L: 0x01, +} + +_GLUCOSE_UNIT_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'unit' / construct.SymmetricMapping( + construct.Byte, _GLUCOSE_MAPPING), + construct.Padding(3), +) + +_ZERO_LOG_REQUEST = construct.Const(b'\x05\x1A') + +_READING_COUNT_RESPONSE = construct.Struct( + construct.Const(b'\x05\x0f'), + 'count' / construct.Int16ul, +) + +_READ_RECORD_REQUEST = construct.Struct( + construct.Const(b'\x05\x1f'), + 'record_id' / construct.Int16ul, +) + +_READING_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'timestamp' / _TIMESTAMP_ADAPTER, + 'value' / construct.Int32ul, +) class Device(serial.SerialDevice): - BAUDRATE = 9600 - DEFAULT_CABLE_ID = '067b:2303' # Generic PL2303 cable. - - def __init__(self, device): - super(Device, self).__init__(device) - - self.sent_counter_ = False - self.expect_receive_ = False - - def connect(self): - self._send_command('', disconnect=True) - - def disconnect(self): - self.connect() - - def _read_response(self): - response = _Packet() - - response.read_from(self.serial_) - - if not response.disconnect and response.sent_counter != self.expect_receive_: - raise lifescan.MalformedCommand( - 'at position 2[0b] expected %02x, received %02x' % ( - self.expect_receive_, response.expect_receive)) + BAUDRATE = 9600 + DEFAULT_CABLE_ID = '067b:2303' # Generic PL2303 cable. + TIMEOUT = 0.5 - if not response.acknowledge: - self.expect_receive_ = not self.expect_receive_ + def __init__(self, device): + super(Device, self).__init__(device) - response.validate_checksum() + self.sent_counter_ = False + self.expect_receive_ = False + self.buffered_reader_ = construct.Rebuffered(_PACKET, tailcutoff=1024) - if not response.acknowledge: - self._send_command('', acknowledge=True) + def connect(self): + self._send_packet(b'', disconnect=True) + self._read_ack() - return response + def disconnect(self): + self.connect() - def _send_command(self, cmd_bytes, acknowledge=False, disconnect=False): - cmd = _Packet() + def _send_packet(self, message, acknowledge=False, disconnect=False): + pkt = _PACKET.build( + {'value': { + 'message': message, + 'sequence_number': self.sent_counter_, + 'expect_receive': self.expect_receive_, + 'acknowledge': acknowledge, + 'disconnect': disconnect, + }}) + logging.debug('sending packet: %s', binascii.hexlify(pkt)) - # set the proper expectations - cmd.build_command(cmd_bytes) - cmd.sent_counter = self.sent_counter_ - cmd.expect_receive = self.expect_receive_ - cmd.acknowledge = acknowledge - cmd.disconnect = disconnect + self.serial_.write(pkt) + self.serial_.flush() - cmd.update_checksum() + def _read_packet(self): + raw_pkt = self.buffered_reader_.parse_stream(self.serial_) + logging.debug('received packet: %r', raw_pkt) - self.serial_.write(cmd.tobytes()) - self.serial_.flush() + # discard the checksum and copy + pkt = raw_pkt.value - if not acknowledge: - self.sent_counter_ = not self.sent_counter_ - result = self._read_response() - return result + if not pkt.disconnect and pkt.sequence_number != self.expect_receive_: + raise lifescan.MalformedCommand( + 'at position 2[0b] expected %02x, received %02x' % ( + self.expect_receive_, pkt.sequence_count)) - def get_meter_info(self): - return common.MeterInfo( - 'OneTouch Ultra Easy glucometer', - serial_number=self.get_serial_number(), - version_info=( - 'Software version: ' + self.get_version(),), - native_unit=self.get_glucose_unit()) + return pkt - def get_version(self): - result = self._send_command(_READ_VERSION) + def _send_ack(self): + self._send_packet(b'', acknowledge=True, disconnect=False) - response = self._read_response() + def _read_ack(self): + pkt = self._read_packet() + assert pkt.acknowledge - return response.data[3:].tobytes().decode('ascii') + def _send_request(self, request_format, *args): + request = request_format.build(*args) + self._send_packet(request, acknowledge=False, disconnect=False) - def get_serial_number(self): - result = self._send_command(_READ_SERIAL_NUMBER) + self.sent_counter_ = not self.sent_counter_ + self._read_ack() - response = self._read_response() + def _read_response(self, response_format): + pkt = self._read_packet() + assert not pkt.acknowledge - return response.data[2:].tobytes().decode('ascii') + self.expect_receive_ = not self.expect_receive_ + self._send_ack() - def get_datetime(self): - result = self._send_command(_READ_DATETIME) - response = self._read_response() + return response_format.parse(pkt.message) - return _convert_timestamp(response.data[2:6]) + def get_meter_info(self): + return common.MeterInfo( + 'OneTouch Ultra Easy glucometer', + serial_number=self.get_serial_number(), + version_info=( + 'Software version: ' + self.get_version(),), + native_unit=self.get_glucose_unit()) - def set_datetime(self, date=datetime.datetime.now()): - epoch = datetime.datetime.utcfromtimestamp(0) - delta = date - epoch - timestamp = int(delta.total_seconds()) + def get_version(self): + self._send_request(_VERSION_REQUEST, None) - timestamp_bytes = _STRUCT_TIMESTAMP.pack(timestamp) + response = self._read_response(_VERSION_RESPONSE) - result = self._send_command(_WRITE_DATETIME + timestamp_bytes) + return response.version - response = self._read_response() - return _convert_timestamp(response.data[2:6]) + def get_serial_number(self): + self._send_request(_SERIAL_NUMBER_REQUEST, None) - def zero_log(self): - result = self._send_command(_DELETE_RECORDS) - response = self._read_response() + response = self._read_response(_SERIAL_NUMBER_RESPONSE) + return response.serial_number - if response.data.tobytes() != b'\x05\x06': - raise exceptions.InvalidResponse(response.data) + def get_datetime(self): + self._send_request( + _DATETIME_REQUEST, {'request_type': 'read'}) + response = self._read_response(_DATETIME_RESPONSE) + return response.timestamp - def get_glucose_unit(self): - result = self._send_command(_READ_GLUCOSE_UNIT) - response = self._read_response() + def set_datetime(self, date=datetime.datetime.now()): + self._send_request(_DATETIME_REQUEST, { + 'request_type': 'write', + 'timestamp': date, + }) - if response.data[2] == 0: - return common.Unit.MG_DL - elif response.data[2] == 1: - return common.Unit.MMOL_L - else: - raise lifescan.MalformedCommand( - 'at position PM1 invalid value %02x for unit' % response.data[2]) + response = self._read_response(_DATETIME_RESPONSE) + return response.timestamp - def _get_reading(self, record_id): - id_bytes = _STRUCT_RECORDID.pack(record_id) + def zero_log(self): + self._send_request(_ZERO_LOG_REQUEST, None) + self._read_response(_COMMAND_SUCCESS) - result = self._send_command(_READ_RECORD + id_bytes) - return self._read_response() + def get_glucose_unit(self): + self._send_request(_GLUCOSE_UNIT_REQUEST, None) + response = self._read_response(_GLUCOSE_UNIT_RESPONSE) - def get_readings(self): - count_response = self._get_reading(_INVALID_RECORD) + return response.unit - record_count, = _STRUCT_RECORDID.unpack_from(count_response.data, 2) + def _get_reading(self, record_id): + self._send_request( + _READ_RECORD_REQUEST, {'record_id': record_id}) + return self._read_response(_READING_RESPONSE) - for record_id in range(record_count): - record_response = self._get_reading(record_id) + def get_readings(self): + self._send_request( + _READ_RECORD_REQUEST, {'record_id': _INVALID_RECORD}) + count_response = self._read_response(_READING_COUNT_RESPONSE) - timestamp = _convert_timestamp(record_response.data[2:6]) - value, = _STRUCT_TIMESTAMP.unpack_from(record_response.data, 6) + for record_id in range(count_response.count): + self._send_request( + _READ_RECORD_REQUEST, {'record_id': record_id}) + reading = self._read_response(_READING_RESPONSE) - yield common.GlucoseReading(timestamp, float(value)) + yield common.GlucoseReading( + reading.timestamp, + float(reading.value)) diff --git a/setup.py b/setup.py index a9b7c18..3be4b94 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def run_tests(self): # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], - 'otultraeasy': ['pyserial'], + 'otultraeasy': ['construct', 'pyserial'], 'otverio2015': ['python-scsi'], 'fsinsulinx': ['construct', 'hidapi'], 'fslibre': ['construct', 'hidapi'], diff --git a/test/test_otultraeasy.py b/test/test_otultraeasy.py index a1d4c02..52a98f1 100644 --- a/test/test_otultraeasy.py +++ b/test/test_otultraeasy.py @@ -38,23 +38,6 @@ def test_crc_array(self): 0x62C2, lifescan.crc_ccitt(cmd_array)) - def test_packet_update_checksum(self): - packet = otultraeasy._Packet() - - packet.build_command('') - packet.disconnect = True - - packet.update_checksum() - self.assertEqual( - b'\x02\x06\x08\x03\xC2\x62', - packet.tobytes()) - - packet.validate_checksum() - packet.disconnect = False - - with self.assertRaises(exceptions.InvalidChecksum): - packet.validate_checksum() - if __name__ == '__main__': unittest.main() From cfbf51d6a090626accfc8437f5bd586112178a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 11:51:23 +0000 Subject: [PATCH 16/58] otultraeasy: factor out the construct Timestamp implementation. This adds tests to ensure this works right in the general case, so that it can be used with different parameters. The adapter will be reused in the otverio2015 driver. --- glucometerutils/drivers/otultraeasy.py | 19 ++---- glucometerutils/support/construct_extras.py | 33 +++++++++ test-requirements.txt | 1 + test/test_construct_extras.py | 75 +++++++++++++++++++++ 4 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 glucometerutils/support/construct_extras.py create mode 100644 test/test_construct_extras.py diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 579c07c..0f95e80 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -25,18 +25,13 @@ import construct from glucometerutils import common +from glucometerutils.support import construct_extras from glucometerutils.support import lifescan from glucometerutils.support import serial _INVALID_RECORD = 501 -_EPOCH = datetime.datetime.utcfromtimestamp(0) - -def datetime_to_timestamp(date): - delta = date - _EPOCH - return int(delta.total_seconds()) - _PACKET = construct.Struct( construct.RawCopy( @@ -63,10 +58,6 @@ def datetime_to_timestamp(date): ) _COMMAND_SUCCESS = construct.Const(b'\x05\x06') -_TIMESTAMP_ADAPTER = construct.ExprAdapter( - construct.Int32ul, - encoder=lambda obj, ctx: datetime_to_timestamp(obj), - decoder=lambda obj, ctx: datetime.datetime.fromtimestamp(obj)) _VERSION_REQUEST = construct.Const(b'\x05\x0d\x02') @@ -86,12 +77,14 @@ def datetime_to_timestamp(date): _DATETIME_REQUEST = construct.Struct( construct.Const(b'\x05\x20'), # 0x20 is the datetime 'request_type' / construct.Enum(construct.Byte, write=0x01, read=0x02), - 'timestamp' / construct.Default(_TIMESTAMP_ADAPTER, _EPOCH), + 'timestamp' / construct.Default( + construct_extras.Timestamp(construct.Int32ul), + datetime.datetime(1970, 1, 1, 0, 0)), ) _DATETIME_RESPONSE = construct.Struct( _COMMAND_SUCCESS, - 'timestamp' / _TIMESTAMP_ADAPTER, + 'timestamp' / construct_extras.Timestamp(construct.Int32ul), ) _GLUCOSE_UNIT_REQUEST = construct.Const( @@ -123,7 +116,7 @@ def datetime_to_timestamp(date): _READING_RESPONSE = construct.Struct( _COMMAND_SUCCESS, - 'timestamp' / _TIMESTAMP_ADAPTER, + 'timestamp' / construct_extras.Timestamp(construct.Int32ul), 'value' / construct.Int32ul, ) diff --git a/glucometerutils/support/construct_extras.py b/glucometerutils/support/construct_extras.py new file mode 100644 index 0000000..cb42105 --- /dev/null +++ b/glucometerutils/support/construct_extras.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Extra classes for Construct.""" + +__author__ = 'Diego Elio Pettenò' +__email__ = 'flameeyes@flameeyes.eu' +__copyright__ = 'Copyright © 2018, Diego Elio Pettenò' +__license__ = 'MIT' + +import datetime + +import construct + +class Timestamp(construct.Adapter): + """Adapter for converting datetime object into timestamps. + + Take two parameters: the subcon object to output the resulting timestamp as, + and an optional epoch offset to the UNIX Epoch. + + """ + __slots__ = ["epoch"] + + def __init__(self, subcon, epoch=0): + super(Timestamp, self).__init__(subcon) + self.epoch = epoch + + def _encode(self, obj, context): + assert isinstance(obj, datetime.datetime) + epoch_date = datetime.datetime.utcfromtimestamp(self.epoch) + delta = obj - epoch_date + return int(delta.total_seconds()) + + def _decode(self, obj, context): + return datetime.datetime.utcfromtimestamp(obj + self.epoch) diff --git a/test-requirements.txt b/test-requirements.txt index 9f7c85b..29ac573 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,5 @@ absl-py +construct pytest pytest-timeout pyserial diff --git a/test/test_construct_extras.py b/test/test_construct_extras.py new file mode 100644 index 0000000..faccabf --- /dev/null +++ b/test/test_construct_extras.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +"""Tests for the common routines.""" + +__author__ = 'Diego Elio Pettenò' +__email__ = 'flameeyes@flameeyes.eu' +__copyright__ = 'Copyright © 2018, Diego Elio Pettenò' +__license__ = 'MIT' + +import datetime +import os +import sys +import unittest + +import construct + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from glucometerutils.support import construct_extras + + +_TEST_DATE1 = datetime.datetime(1970, 1, 2, 0, 0) +_TEST_DATE2 = datetime.datetime(1971, 1, 1, 0, 0) +_TEST_DATE3 = datetime.datetime(1970, 1, 1, 0, 0) + +_NEW_EPOCH = 31536000 # datetime.datetime(1971, 1, 1, 0, 0) + +class TestTimestamp(unittest.TestCase): + + def test_build_unix_epoch(self): + self.assertEqual( + construct_extras.Timestamp(construct.Int32ul).build(_TEST_DATE1), + b'\x80\x51\x01\x00') + + def test_parse_unix_epoch(self): + self.assertEqual( + construct_extras.Timestamp(construct.Int32ul).parse( + b'\x803\xe1\x01'), + _TEST_DATE2) + + def test_build_custom_epoch(self): + self.assertEqual( + construct_extras.Timestamp( + construct.Int32ul, epoch=_NEW_EPOCH).build(_TEST_DATE2), + b'\x00\x00\x00\x00') + + def test_parse_custom_epoch(self): + self.assertEqual( + construct_extras.Timestamp( + construct.Int32ul, epoch=_NEW_EPOCH).parse( + b'\x00\x00\x00\x00'), + _TEST_DATE2) + + def test_build_custom_epoch_negative_failure(self): + with self.assertRaises(construct.core.FieldError): + construct_extras.Timestamp( + construct.Int32ul, epoch=_NEW_EPOCH).build(_TEST_DATE1) + + def test_build_custom_epoch_negative_success(self): + self.assertEqual( + construct_extras.Timestamp( + construct.Int32sl, epoch=_NEW_EPOCH).build(_TEST_DATE1), + b'\x00\x1e\x20\xfe') + + def test_build_varint(self): + self.assertEqual( + construct_extras.Timestamp(construct.VarInt).build(_TEST_DATE3), + b'\x00') + + def test_invalid_value(self): + with self.assertRaises(AssertionError): + construct_extras.Timestamp(construct.Int32ul).build('foo') + + +if __name__ == '__main__': + unittest.main() From 7f6a3eeca6aff196e68215d99bb6d48d57d069c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 12:46:48 +0000 Subject: [PATCH 17/58] otverio2015: rewrite using construct. This simplifies the code a bit here and there, making sure that the structures are all define at the top of the file. It also align the structure of the driver a bit more with otultraeasy, making it easier to spot the similitudes. --- README | 30 +- glucometerutils/drivers/otverio2015.py | 436 ++++++++++++------------- setup.py | 2 +- 3 files changed, 232 insertions(+), 236 deletions(-) diff --git a/README b/README index 947348c..2e7a460 100644 --- a/README +++ b/README @@ -32,21 +32,21 @@ $ . glucometerutils-venv/bin/activate Please see the following table for the driver for each device that is known and supported. -| Manufacturer | Model Name | Driver | Dependencies | -| --- | --- | --- | --- | -| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | -| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | -| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | -| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | -| Roche | Accu-Chek Mobile | `accuchek_reports` | | -| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | +| Manufacturer | Model Name | Driver | Dependencies | +| --- | --- | --- | --- | +| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | +| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | +| LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | +| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | +| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | +| Roche | Accu-Chek Mobile | `accuchek_reports` | | +| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | † Untested. ‡ Optional dependency on Linux; required on other operating systems. diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 5925a58..8d87409 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -28,246 +28,242 @@ import binascii import datetime import logging -import struct +import construct from pyscsi.pyscsi.scsi import SCSI from pyscsi.pyscsi.scsi_device import SCSIDevice from glucometerutils import common from glucometerutils import exceptions +from glucometerutils.support import construct_extras from glucometerutils.support import lifescan -# Match the same values in the otultraeasy driver. -_STX = 0x02 -_ETX = 0x03 - # This device uses SCSI blocks as registers. _REGISTER_SIZE = 512 -_STRUCT_PREAMBLE = struct.Struct(' _REGISTER_SIZE: - raise lifescan.MalformedCommand( - 'invalid length: %d > REGISTER_SIZE' % length) +_READ_UNIT_RESPONSE = construct.Struct( + construct.Const(b'\x03\x06'), # different from _COMMAND_SUCCESS + 'unit' / construct.SymmetricMapping( + construct.Byte, _GLUCOSE_MAPPING), + construct.Padding(3), +) - # 2 is the length of the checksum, so it should be ignored. - calculated_checksum = lifescan.crc_ccitt(register[:(length-2)]) +_READ_RTC_REQUEST = construct.Const(b'\x04\x20\x02') - coda_offset = length - _STRUCT_CODA.size - etx, encoded_checksum = _STRUCT_CODA.unpack_from(register[coda_offset:]) - if etx != _ETX: - raise lifescan.MalformedCommand( - 'invalid ETX byte: %02x' % etx) - if encoded_checksum != calculated_checksum: - raise exceptions.InvalidChecksum(encoded_checksum, calculated_checksum) +_READ_RTC_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'timestamp' / _TIMESTAMP, +) - response = register[_STRUCT_PREAMBLE.size:coda_offset] +_WRITE_RTC_REQUEST = construct.Struct( + construct.Const(b'\x04\x20\x01'), + 'timestamp' / _TIMESTAMP, +) - logging.debug('Read packet: %s' % binascii.hexlify(response)) - return response +_MEMORY_ERASE_REQUEST = construct.Const(b'\x04\x1a') -def _encode_message(cmd): - """Add message preamble and calculate checksum, add padding.""" - length = len(cmd) + _STRUCT_PREAMBLE.size + _STRUCT_CODA.size - preamble = _STRUCT_PREAMBLE.pack(_STX, length) - message = preamble + cmd + bytes((_ETX,)) - checksum = _STRUCT_CHECKSUM.pack(lifescan.crc_ccitt(message)) - message += checksum +_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x04\x27\x00') - logging.debug('Sending packet: %s' % binascii.hexlify(message)) +_READ_RECORD_COUNT_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'count' / construct.Int16ul, +) - # Pad the message to match the size of the register. - return message + bytes(_REGISTER_SIZE - len(message)) +_READ_RECORD_REQUEST = construct.Struct( + construct.Const(b'\x04\x31\x02'), + 'record_id' / construct.Int16ul, + construct.Const(b'\x00'), +) + +_MEAL_FLAG = { + common.Meal.NONE: 0x00, + common.Meal.BEFORE: 0x01, + common.Meal.AFTER: 0x02, +} -def _convert_timestamp(timestamp): - return datetime.datetime.utcfromtimestamp(timestamp + _EPOCH_BASE) +_READ_RECORD_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'inverse_counter' / construct.Int16ul, + construct.Padding(1), + 'lifetime_counter' / construct.Int16ul, + 'timestamp' / _TIMESTAMP, + 'value' / construct.Int16ul, + 'meal' / construct.SymmetricMapping( + construct.Byte, _MEAL_FLAG), + construct.Padding(4), +) class Device(object): - def __init__(self, device): - if not device: - raise exceptions.CommandLineError( - '--device parameter is required, should point to the disk device ' - 'representing the meter.') - - self.device_name_ = device - self.scsi_device_ = SCSIDevice(device, readwrite=True) - self.scsi_ = SCSI(self.scsi_device_) - self.scsi_.blocksize = _REGISTER_SIZE - - def _send_message(self, cmd, lba): - """Send a request to the meter, and read its response. - - Args: - cmd: (bytes) the raw command to send the device, without - preamble or checksum. - lba: (int) the address of the block register to use, known - valid addresses are 3, 4 and 5. - - Returns: - (bytes) The raw response from the meter. No preamble or coda is - present, and the checksum has already been validated. - """ - self.scsi_.write10(lba, 1, _encode_message(cmd)) - response = self.scsi_.read10(lba, 1) - # TODO: validate that the response is valid. - return _extract_message(response.datain) - - def connect(self): - inq = self.scsi_.inquiry() - vendor = inq.result['t10_vendor_identification'][:32] - if vendor != b'LifeScan': - raise exceptions.ConnectionFailed( - 'Device %s is not a LifeScan glucometer.' % self.device_name_) - - def disconnect(self): - return - - def get_meter_info(self): - return common.MeterInfo( - 'OneTouch %s glucometer' % self._query_string(_QUERY_KEY_MODEL), - serial_number=self.get_serial_number(), - version_info=( - 'Software version: ' + self.get_version(),), - native_unit=self.get_glucose_unit()) - - def _query_string(self, query_key): - response = self._send_message(_QUERY_REQUEST + query_key, 3) - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - # Strings are encoded in wide characters (LE), but they should - # only contain ASCII characters. Note that the string is - # null-terminated, so the last character should be dropped. - return response[2:].decode('utf-16-le')[:-1] - - def _read_parameter(self, parameter_key): - response = self._send_message( - _READ_PARAMETER_REQUEST + parameter_key, 4) - if response[0:2] != b'\x03\x06': - raise lifescan.MalformedCommand( - 'invalid response, expected 03 06, received %02x %02x' % ( - response[0], response[1])) - return response[2:] - - def get_serial_number(self): - return self._query_string(_QUERY_KEY_SERIAL) - - def get_version(self): - return self._query_string(_QUERY_KEY_SOFTWARE) - - def get_datetime(self): - response = self._send_message(_READ_RTC_REQUEST, 3) - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - (timestamp,) = _STRUCT_TIMESTAMP.unpack(response[2:]) - return _convert_timestamp(timestamp) - - def set_datetime(self, date=datetime.datetime.now()): - epoch = datetime.datetime.utcfromtimestamp(_EPOCH_BASE) - delta = date - epoch - timestamp = int(delta.total_seconds()) - - timestamp_bytes = _STRUCT_TIMESTAMP.pack(timestamp) - response = self._send_message(_WRITE_RTC_REQUEST + timestamp_bytes, 3) - - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - - # The device does not return the new datetime, so confirm by - # calling READ RTC again. - return self.get_datetime() - - def zero_log(self): - response = self._send_message(_MEMORY_ERASE_REQUEST, 3) - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - - def _get_reading_count(self): - response = self._send_message(_READ_RECORD_COUNT_REQUEST, 3) - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - - (record_count,) = _STRUCT_RECORDID.unpack(response[2:]) - return record_count - - def get_glucose_unit(self): - unit_value = self._read_parameter(_PARAMETER_KEY_UNIT) - if unit_value == b'\x00\x00\x00\x00': - return common.Unit.MG_DL - elif unit_value == b'\x01\x00\x00\x00': - return common.Unit.MMOL_L - else: - raise exceptions.InvalidGlucoseUnit('%r' % unit_value) - - def _get_reading(self, record_number): - request = (_READ_RECORD_REQUEST_PREFIX + - _STRUCT_RECORDID.pack(record_number) + - _READ_RECORD_REQUEST_SUFFIX) - response = self._send_message(request, 3) - if response[0:2] != b'\x04\06': - raise lifescan.MalformedCommand( - 'invalid response, expected 04 06, received %02x %02x' % ( - response[0], response[1])) - - (unused_const1, unused_const2, unused_counter, unused_const3, - unused_counter2, timestamp, value, meal_flag, unused_const4, unused_flags, - unused_const5, unused_const6) = _STRUCT_RECORD.unpack( - response) - - return common.GlucoseReading( - _convert_timestamp(timestamp), float(value), meal=_MEAL_CODES[meal_flag]) - - def get_readings(self): - record_count = self._get_reading_count() - for record_number in range(record_count): - yield self._get_reading(record_number) + def __init__(self, device): + if not device: + raise exceptions.CommandLineError( + '--device parameter is required, should point to the disk ' + 'device representing the meter.') + + self.device_name_ = device + self.scsi_device_ = SCSIDevice(device, readwrite=True) + self.scsi_ = SCSI(self.scsi_device_) + self.scsi_.blocksize = _REGISTER_SIZE + + def connect(self): + inq = self.scsi_.inquiry() + logging.debug('Device connected: %r', inq.result) + vendor = inq.result['t10_vendor_identification'][:32] + if vendor != b'LifeScan': + raise exceptions.ConnectionFailed( + 'Device %s is not a LifeScan glucometer.' % self.device_name_) + + def disconnect(self): + return + + def _send_request(self, lba, request_format, request_obj, response_format): + """Send a request to the meter, and read its response. + + Args: + lba: (int) the address of the block register to use, known + valid addresses are 3, 4 and 5. + request_format: a construct format identifier of the request to send + request_obj: the object to format with the provided identifier + response_format: a construct format identifier to parse the returned + message with. + + Returns: + The Container object parsed from the response received by the meter. + + Raises: + lifescan.MalformedCommand if Construct fails to build the request or + parse the response. + + """ + try: + request = request_format.build(request_obj) + request_raw = _PACKET.build({'value': {'message': request}}) + logging.debug( + 'Request sent: %s', binascii.hexlify(request_raw)) + self.scsi_.write10(lba, 1, request_raw) + + response_raw = self.scsi_.read10(lba, 1) + logging.debug( + 'Response received: %s', binascii.hexlify(response_raw.datain)) + response_pkt = _PACKET.parse(response_raw.datain) + logging.debug('Response packet: %r', response_pkt) + + response = response_format.parse(response_pkt.value.message) + logging.debug('Response parsed: %r', response) + + return response + except construct.ConstructError as e: + raise lifescan.MalformedCommand(str(e)) + + def _query_string(self, selector): + response = self._send_request( + 3, _QUERY_REQUEST, {'selector': selector}, _QUERY_RESPONSE) + + # Unfortunately the CString implementation in construct does not support + # multi-byte encodings, so we need to discard the terminating null byte + # ourself. + return response.value[:-1] + + def get_meter_info(self): + return common.MeterInfo( + 'OneTouch %s glucometer' % self._query_string('model'), + serial_number=self.get_serial_number(), + version_info=( + 'Software version: ' + self.get_version(),), + native_unit=self.get_glucose_unit()) + + def get_serial_number(self): + return self._query_string('serial') + + def get_version(self): + return self._query_string('software') + + def get_datetime(self): + response = self._send_request( + 3, _READ_RTC_REQUEST, None, _READ_RTC_RESPONSE) + return response.timestamp + + def set_datetime(self, date=datetime.datetime.now()): + self._send_request( + 3, _WRITE_RTC_REQUEST, {'timestamp': date}, + _COMMAND_SUCCESS) + + # The device does not return the new datetime, so confirm by calling + # READ RTC again. + return self.get_datetime() + + def zero_log(self): + self._send_request( + 3, _MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) + + def _get_reading_count(self): + response = self._send_request( + 3, _READ_RECORD_COUNT_REQUEST, None, _READ_RECORD_COUNT_RESPONSE) + return response.count + + def get_glucose_unit(self): + response = self._send_request( + 4, _READ_PARAMETER_REQUEST, {'selector': 'unit'}, + _READ_UNIT_RESPONSE) + return response.unit + + def _get_reading(self, record_id): + response = self._send_request( + 3, _READ_RECORD_REQUEST, {'record_id': record_id}, + _READ_RECORD_RESPONSE) + return common.GlucoseReading( + response.timestamp, float(response.value), meal=response.meal) + + def get_readings(self): + record_count = self._get_reading_count() + for record_id in range(record_count): + yield self._get_reading(record_id) diff --git a/setup.py b/setup.py index 3be4b94..212425a 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(self): # listed as mandatory for the feature. 'otultra2': ['pyserial'], 'otultraeasy': ['construct', 'pyserial'], - 'otverio2015': ['python-scsi'], + 'otverio2015': ['construct', 'python-scsi'], 'fsinsulinx': ['construct', 'hidapi'], 'fslibre': ['construct', 'hidapi'], 'fsoptium': ['pyserial'], From 44bc1fad54b171bb758eb336796a487ca28157f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 12:50:17 +0000 Subject: [PATCH 18/58] otultraeasy: wrap around construct exceptions to MalformedCommand. --- glucometerutils/drivers/otultraeasy.py | 31 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 0f95e80..ced1f0b 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -133,8 +133,11 @@ def __init__(self, device): self.buffered_reader_ = construct.Rebuffered(_PACKET, tailcutoff=1024) def connect(self): - self._send_packet(b'', disconnect=True) - self._read_ack() + try: + self._send_packet(b'', disconnect=True) + self._read_ack() + except construct.ConstructError as e: + raise lifescan.MalformedCommand(str(e)) def disconnect(self): self.connect() @@ -175,20 +178,26 @@ def _read_ack(self): assert pkt.acknowledge def _send_request(self, request_format, *args): - request = request_format.build(*args) - self._send_packet(request, acknowledge=False, disconnect=False) + try: + request = request_format.build(*args) + self._send_packet(request, acknowledge=False, disconnect=False) - self.sent_counter_ = not self.sent_counter_ - self._read_ack() + self.sent_counter_ = not self.sent_counter_ + self._read_ack() + except construct.ConstructError as e: + raise lifescan.MalformedCommand(str(e)) def _read_response(self, response_format): - pkt = self._read_packet() - assert not pkt.acknowledge + try: + pkt = self._read_packet() + assert not pkt.acknowledge - self.expect_receive_ = not self.expect_receive_ - self._send_ack() + self.expect_receive_ = not self.expect_receive_ + self._send_ack() - return response_format.parse(pkt.message) + return response_format.parse(pkt.message) + except construct.ConstructError as e: + raise lifescan.MalformedCommand(str(e)) def get_meter_info(self): return common.MeterInfo( From 90323968fc494a86e140502f5bb4b0a0d127f634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 12:53:59 +0000 Subject: [PATCH 19/58] otultraeasy: rename _ZERO_LOG_REQUEST to _MEMORY_ERASE_REQUEST. This makes it the same as the otverio2015. --- glucometerutils/drivers/otultraeasy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index ced1f0b..38bd1aa 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -102,7 +102,7 @@ construct.Padding(3), ) -_ZERO_LOG_REQUEST = construct.Const(b'\x05\x1A') +_MEMORY_ERASE_REQUEST = construct.Const(b'\x05\x1A') _READING_COUNT_RESPONSE = construct.Struct( construct.Const(b'\x05\x0f'), @@ -236,7 +236,7 @@ def set_datetime(self, date=datetime.datetime.now()): return response.timestamp def zero_log(self): - self._send_request(_ZERO_LOG_REQUEST, None) + self._send_request(_MEMORY_ERASE_REQUEST, None) self._read_response(_COMMAND_SUCCESS) def get_glucose_unit(self): From c3d89aae25d56144647bf7fa577389afe3522212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 13:12:00 +0000 Subject: [PATCH 20/58] otultraeasy: merge _send_request and _read_response and match otverio2015. There was always a 1-to-1 mapping between these two functions, so merge them into a single function that knows both the request and response format. It also includes some refactoring of the actual record reading, to match the same structure of functions in otverio2015. --- glucometerutils/drivers/otultraeasy.py | 76 ++++++++++++-------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 38bd1aa..57c7ea2 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -177,25 +177,21 @@ def _read_ack(self): pkt = self._read_packet() assert pkt.acknowledge - def _send_request(self, request_format, *args): + def _send_request(self, request_format, request_obj, response_format): try: - request = request_format.build(*args) + request = request_format.build(request_obj) self._send_packet(request, acknowledge=False, disconnect=False) self.sent_counter_ = not self.sent_counter_ self._read_ack() - except construct.ConstructError as e: - raise lifescan.MalformedCommand(str(e)) - def _read_response(self, response_format): - try: - pkt = self._read_packet() - assert not pkt.acknowledge + response_pkt = self._read_packet() + assert not response_pkt.acknowledge self.expect_receive_ = not self.expect_receive_ self._send_ack() - return response_format.parse(pkt.message) + return response_format.parse(response_pkt.message) except construct.ConstructError as e: raise lifescan.MalformedCommand(str(e)) @@ -208,58 +204,56 @@ def get_meter_info(self): native_unit=self.get_glucose_unit()) def get_version(self): - self._send_request(_VERSION_REQUEST, None) - - response = self._read_response(_VERSION_RESPONSE) + response = self._send_request( + _VERSION_REQUEST, None, _VERSION_RESPONSE) return response.version def get_serial_number(self): - self._send_request(_SERIAL_NUMBER_REQUEST, None) + response = self._send_request( + _SERIAL_NUMBER_REQUEST, None, _SERIAL_NUMBER_RESPONSE) - response = self._read_response(_SERIAL_NUMBER_RESPONSE) return response.serial_number def get_datetime(self): - self._send_request( - _DATETIME_REQUEST, {'request_type': 'read'}) - response = self._read_response(_DATETIME_RESPONSE) + response = self._send_request( + _DATETIME_REQUEST, {'request_type': 'read'}, + _DATETIME_RESPONSE) + return response.timestamp def set_datetime(self, date=datetime.datetime.now()): - self._send_request(_DATETIME_REQUEST, { - 'request_type': 'write', - 'timestamp': date, - }) + response = self._send_request( + _DATETIME_REQUEST, { + 'request_type': 'write', + 'timestamp': date, + }, _DATETIME_RESPONSE) - response = self._read_response(_DATETIME_RESPONSE) return response.timestamp def zero_log(self): - self._send_request(_MEMORY_ERASE_REQUEST, None) - self._read_response(_COMMAND_SUCCESS) + self._send_request(_MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) def get_glucose_unit(self): - self._send_request(_GLUCOSE_UNIT_REQUEST, None) - response = self._read_response(_GLUCOSE_UNIT_RESPONSE) + response = self._send_request( + _GLUCOSE_UNIT_REQUEST, None, _GLUCOSE_UNIT_RESPONSE) return response.unit + def _get_reading_count(self): + response = self._send_request( + _READ_RECORD_REQUEST, {'record_id': _INVALID_RECORD}, + _READING_COUNT_RESPONSE) + return response.count + def _get_reading(self, record_id): - self._send_request( - _READ_RECORD_REQUEST, {'record_id': record_id}) - return self._read_response(_READING_RESPONSE) + response = self._send_request( + _READ_RECORD_REQUEST, {'record_id': record_id}, _READING_RESPONSE) + + return common.GlucoseReading( + response.timestamp, float(response.value)) def get_readings(self): - self._send_request( - _READ_RECORD_REQUEST, {'record_id': _INVALID_RECORD}) - count_response = self._read_response(_READING_COUNT_RESPONSE) - - for record_id in range(count_response.count): - self._send_request( - _READ_RECORD_REQUEST, {'record_id': record_id}) - reading = self._read_response(_READING_RESPONSE) - - yield common.GlucoseReading( - reading.timestamp, - float(reading.value)) + record_count = self._get_reading_count() + for record_id in range(record_count): + yield self._get_reading(record_id) From efd42641733b7a172f8081d6da9f9329c00f0e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 13:13:25 +0000 Subject: [PATCH 21/58] otverio2015: reorder functions to match otultraeasy. --- glucometerutils/drivers/otverio2015.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 8d87409..2c5b52f 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -245,17 +245,17 @@ def zero_log(self): self._send_request( 3, _MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) - def _get_reading_count(self): - response = self._send_request( - 3, _READ_RECORD_COUNT_REQUEST, None, _READ_RECORD_COUNT_RESPONSE) - return response.count - def get_glucose_unit(self): response = self._send_request( 4, _READ_PARAMETER_REQUEST, {'selector': 'unit'}, _READ_UNIT_RESPONSE) return response.unit + def _get_reading_count(self): + response = self._send_request( + 3, _READ_RECORD_COUNT_REQUEST, None, _READ_RECORD_COUNT_RESPONSE) + return response.count + def _get_reading(self, record_id): response = self._send_request( 3, _READ_RECORD_REQUEST, {'record_id': record_id}, From 60eef1be7ebfce29b79f2eb88e076c63cd0290e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 14:48:16 +0000 Subject: [PATCH 22/58] test_lifescan: rename from test_otultraeasy, and cleanup. This test was actually only testing the CRC CCITT implementation now that otultraeasy uses construct. --- test/{test_otultraeasy.py => test_lifescan.py} | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) rename test/{test_otultraeasy.py => test_lifescan.py} (65%) diff --git a/test/test_otultraeasy.py b/test/test_lifescan.py similarity index 65% rename from test/test_otultraeasy.py rename to test/test_lifescan.py index 52a98f1..5781829 100644 --- a/test/test_otultraeasy.py +++ b/test/test_lifescan.py @@ -10,23 +10,12 @@ import os import sys import unittest -from unittest import mock sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from glucometerutils.drivers import otultraeasy from glucometerutils.support import lifescan -from glucometerutils import exceptions - -class TestOTUltraMini(unittest.TestCase): - def setUp(self): - self.addCleanup(mock.patch.stopall) - - mock_serial = mock.patch('serial.Serial').start() - self.mock_readline = mock_serial.return_value.readline - - self.device = otultraeasy.Device('mockdevice') +class TestChecksum(unittest.TestCase): def test_crc(self): self.assertEqual( 0x41cd, From bf2df602e99e722902f8cf1259bb23baf175255d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 1 Jan 2018 14:54:18 +0000 Subject: [PATCH 23/58] tests: improve code quality by passing the linter. --- test/test_common.py | 7 +++---- test/test_construct_extras.py | 2 +- test/test_otultra2.py | 4 +++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_common.py b/test/test_common.py index 8cff341..4b062ac 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -3,7 +3,7 @@ __author__ = 'Diego Elio Pettenò' __email__ = 'flameeyes@flameeyes.eu' -__copyright__ = 'Copyright © 2013, Diego Elio Pettenò' +__copyright__ = 'Copyright © 2013-2018, Diego Elio Pettenò' __license__ = 'MIT' import datetime @@ -16,7 +16,6 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from glucometerutils import common -from glucometerutils import exceptions class TestGlucoseConversion(parameterized.TestCase): @@ -55,7 +54,7 @@ def test_invalid_values(self, from_unit, to_unit): class TestGlucoseReading(parameterized.TestCase): - TEST_DATETIME = datetime.datetime(2018, 1 ,1, 0, 30, 45) + TEST_DATETIME = datetime.datetime(2018, 1, 1, 0, 30, 45) def test_minimal(self): reading = common.GlucoseReading(self.TEST_DATETIME, 100) @@ -127,4 +126,4 @@ def test_meter_info(self, kwargs_dict, expected_fragment): if __name__ == '__main__': - unittest.main() + unittest.main() diff --git a/test/test_construct_extras.py b/test/test_construct_extras.py index faccabf..66da5b7 100644 --- a/test/test_construct_extras.py +++ b/test/test_construct_extras.py @@ -72,4 +72,4 @@ def test_invalid_value(self): if __name__ == '__main__': - unittest.main() + unittest.main() diff --git a/test/test_otultra2.py b/test/test_otultra2.py index 9f18a85..b421c79 100644 --- a/test/test_otultra2.py +++ b/test/test_otultra2.py @@ -3,7 +3,7 @@ __author__ = 'Diego Elio Pettenò' __email__ = 'flameeyes@flameeyes.eu' -__copyright__ = 'Copyright © 2013-2017, Diego Elio Pettenò' +__copyright__ = 'Copyright © 2013-2018, Diego Elio Pettenò' __license__ = 'MIT' import os @@ -22,10 +22,12 @@ class TestOTUltra2(parameterized.TestCase): def test_checksum(self): + # pylint: disable=protected-access checksum = otultra2._calculate_checksum(b'T') self.assertEqual(0x0054, checksum) def test_checksum_full(self): + # pylint: disable=protected-access checksum = otultra2._calculate_checksum( b'T "SAT","08/03/13","22:12:00 "') self.assertEqual(0x0608, checksum) From 3999c3eb3c82abc20af618df81631a114504a7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 20:47:13 +0000 Subject: [PATCH 24/58] lifescan_binary_protocol: create a new module to support LifeScan drivers. Both the UltraEasy and Verio 2015 use a similar protocol, with the same base packet serialised to the device. Factor the packet definition out (and make it a bit more generic). Verio IQ (Issue #30) also shares the same base protocol. Also move the definition of VERIO_TIMESTAMP to this common module as it's also shared with the Verio IQ. --- glucometerutils/drivers/otultraeasy.py | 43 ++++----------- glucometerutils/drivers/otverio2015.py | 38 ++++---------- .../support/lifescan_binary_protocol.py | 52 +++++++++++++++++++ 3 files changed, 73 insertions(+), 60 deletions(-) create mode 100644 glucometerutils/support/lifescan_binary_protocol.py diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 57c7ea2..0063ec1 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -15,7 +15,7 @@ __author__ = 'Diego Elio Pettenò' __email__ = 'flameeyes@flameeyes.eu' -__copyright__ = 'Copyright © 2014-2017, Diego Elio Pettenò' +__copyright__ = 'Copyright © 2014-2018, Diego Elio Pettenò' __license__ = 'MIT' import binascii @@ -27,36 +27,12 @@ from glucometerutils import common from glucometerutils.support import construct_extras from glucometerutils.support import lifescan +from glucometerutils.support import lifescan_binary_protocol from glucometerutils.support import serial _INVALID_RECORD = 501 - -_PACKET = construct.Struct( - construct.RawCopy( - construct.Embedded( - construct.Struct( - construct.Const(b'\x02'), # stx - 'length' / construct.Rebuild( - construct.Byte, lambda ctx: len(ctx.message) + 6), - construct.EmbeddedBitStruct( - construct.Padding(3), - 'more' / construct.Default(construct.Flag, False), - 'disconnect' / construct.Flag, - 'acknowledge' / construct.Flag, - 'expect_receive' / construct.Flag, - 'sequence_number' / construct.Flag, - ), - 'message' / construct.Bytes(length=lambda ctx: ctx.length - 6), - construct.Const(b'\x03'), # etx - ), - ), - ), - 'checksum' / construct.Checksum( - construct.Int16ul, lifescan.crc_ccitt, construct.this.data), -) - _COMMAND_SUCCESS = construct.Const(b'\x05\x06') _VERSION_REQUEST = construct.Const(b'\x05\x0d\x02') @@ -130,7 +106,8 @@ def __init__(self, device): self.sent_counter_ = False self.expect_receive_ = False - self.buffered_reader_ = construct.Rebuffered(_PACKET, tailcutoff=1024) + self.buffered_reader_ = construct.Rebuffered( + lifescan_binary_protocol.PACKET, tailcutoff=1024) def connect(self): try: @@ -143,13 +120,15 @@ def disconnect(self): self.connect() def _send_packet(self, message, acknowledge=False, disconnect=False): - pkt = _PACKET.build( + pkt = lifescan_binary_protocol.PACKET.build( {'value': { 'message': message, - 'sequence_number': self.sent_counter_, - 'expect_receive': self.expect_receive_, - 'acknowledge': acknowledge, - 'disconnect': disconnect, + 'link_control': { + 'sequence_number': self.sent_counter_, + 'expect_receive': self.expect_receive_, + 'acknowledge': acknowledge, + 'disconnect': disconnect, + }, }}) logging.debug('sending packet: %s', binascii.hexlify(pkt)) diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 2c5b52f..d0b8a9c 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -22,7 +22,7 @@ __author__ = 'Diego Elio Pettenò' __email__ = 'flameeyes@flameeyes.eu' -__copyright__ = 'Copyright © 2016-2017, Diego Elio Pettenò' +__copyright__ = 'Copyright © 2016-2018, Diego Elio Pettenò' __license__ = 'MIT' import binascii @@ -35,38 +35,17 @@ from glucometerutils import common from glucometerutils import exceptions -from glucometerutils.support import construct_extras from glucometerutils.support import lifescan +from glucometerutils.support import lifescan_binary_protocol # This device uses SCSI blocks as registers. _REGISTER_SIZE = 512 _PACKET = construct.Padded( - 512, construct.Struct( - construct.RawCopy( - construct.Embedded( - construct.Struct( - construct.Const(b'\x02'), # stx - 'length' / construct.Rebuild( - construct.Int16ul, lambda ctx: len(ctx.message) + 6), - 'message' / construct.Bytes( - length=lambda ctx: ctx.length - 6), - construct.Const(b'\x03'), # etx - ), - ), - ), - 'checksum' / construct.Checksum( - construct.Int16ul, lifescan.crc_ccitt, construct.this.data), - ), -) + _REGISTER_SIZE, construct.Embedded(lifescan_binary_protocol.PACKET)) _COMMAND_SUCCESS = construct.Const(b'\x04\x06') -# Device-specific timestamp. All timestamp reported by this device are seconds -# since this date. -_TIMESTAMP = construct_extras.Timestamp( - construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 - _QUERY_REQUEST = construct.Struct( construct.Const(b'\x04\xe6\x02'), 'selector' / construct.Enum( @@ -101,12 +80,12 @@ _READ_RTC_RESPONSE = construct.Struct( _COMMAND_SUCCESS, - 'timestamp' / _TIMESTAMP, + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) _WRITE_RTC_REQUEST = construct.Struct( construct.Const(b'\x04\x20\x01'), - 'timestamp' / _TIMESTAMP, + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) _MEMORY_ERASE_REQUEST = construct.Const(b'\x04\x1a') @@ -135,7 +114,7 @@ 'inverse_counter' / construct.Int16ul, construct.Padding(1), 'lifetime_counter' / construct.Int16ul, - 'timestamp' / _TIMESTAMP, + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, 'value' / construct.Int16ul, 'meal' / construct.SymmetricMapping( construct.Byte, _MEAL_FLAG), @@ -186,7 +165,10 @@ def _send_request(self, lba, request_format, request_obj, response_format): """ try: request = request_format.build(request_obj) - request_raw = _PACKET.build({'value': {'message': request}}) + request_raw = _PACKET.build({'value': { + 'message': request, + 'link_control': {}, # Verio does not use link_control. + }}) logging.debug( 'Request sent: %s', binascii.hexlify(request_raw)) self.scsi_.write10(lba, 1, request_raw) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py new file mode 100644 index 0000000..68b030d --- /dev/null +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""Support module for the LifeScan binary protocol. + +A number of LifeScan devices use a semi-compatible binary protocol to talk host +and device, which is (vastly) compatible. + +This module implements an interface to send and receive these messages. +""" + +__author__ = 'Diego Elio Pettenò' +__email__ = 'flameeyes@flameeyes.eu' +__copyright__ = 'Copyright © 2014-2018, Diego Elio Pettenò' +__license__ = 'MIT' + +import construct + +from glucometerutils.support import construct_extras +from glucometerutils.support import lifescan + + +PACKET = construct.Struct( + construct.RawCopy( + construct.Embedded( + construct.Struct( + construct.Const(b'\x02'), # stx + 'length' / construct.Rebuild( + construct.Byte, lambda ctx: len(ctx.message) + 6), + # The following structure is only used by some of the devices. + 'link_control' / construct.BitStruct( + construct.Padding(3), + 'more' / construct.Default( + construct.Flag, False), + 'disconnect' / construct.Default( + construct.Flag, False), + 'acknowledge' / construct.Default( + construct.Flag, False), + 'expect_receive' / construct.Default( + construct.Flag, False), + 'sequence_number' / construct.Default( + construct.Flag, False), + ), + 'message' / construct.Bytes(length=lambda ctx: ctx.length - 6), + construct.Const(b'\x03'), # etx + ), + ), + ), + 'checksum' / construct.Checksum( + construct.Int16ul, lifescan.crc_ccitt, construct.this.data), +) + +VERIO_TIMESTAMP = construct_extras.Timestamp( + construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 From 349b1c794b68fe2dcaad4cd9cf28d09a1d56ee17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 21:10:11 +0000 Subject: [PATCH 25/58] lifescan_binary_protocol: factor out glucose unit mappings. The values are the same between all models sharing this protocol, even though the full reply message isn't. --- glucometerutils/drivers/otultraeasy.py | 7 +------ glucometerutils/drivers/otverio2015.py | 8 +------- glucometerutils/support/lifescan_binary_protocol.py | 9 +++++++++ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 0063ec1..fe7e4ae 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -66,15 +66,10 @@ _GLUCOSE_UNIT_REQUEST = construct.Const( b'\x05\x09\x02\x09\x00\x00\x00\x00') -_GLUCOSE_MAPPING = { - common.Unit.MG_DL: 0x00, - common.Unit.MMOL_L: 0x01, -} _GLUCOSE_UNIT_RESPONSE = construct.Struct( _COMMAND_SUCCESS, - 'unit' / construct.SymmetricMapping( - construct.Byte, _GLUCOSE_MAPPING), + 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index d0b8a9c..9aff4d2 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -64,15 +64,9 @@ construct.Byte, unit=0x04), ) -_GLUCOSE_MAPPING = { - common.Unit.MG_DL: 0x00, - common.Unit.MMOL_L: 0x01, -} - _READ_UNIT_RESPONSE = construct.Struct( construct.Const(b'\x03\x06'), # different from _COMMAND_SUCCESS - 'unit' / construct.SymmetricMapping( - construct.Byte, _GLUCOSE_MAPPING), + 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 68b030d..0143cd4 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -14,6 +14,7 @@ import construct +from glucometerutils import common from glucometerutils.support import construct_extras from glucometerutils.support import lifescan @@ -50,3 +51,11 @@ VERIO_TIMESTAMP = construct_extras.Timestamp( construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 + +_GLUCOSE_UNIT_MAPPING_TABLE = { + common.Unit.MG_DL: 0x00, + common.Unit.MMOL_L: 0x01, +} + +GLUCOSE_UNIT = construct.SymmetricMapping( + construct.Byte, _GLUCOSE_UNIT_MAPPING_TABLE) From 57d719974e9acb40146e41906b3184b2feac7091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 21:37:30 +0000 Subject: [PATCH 26/58] otverioiq: add totally untested driver. This is the first commit to support OneTouch Verio IQ (Issue #30). It's untested despite me having the device because it needs a new kernel I have not planned to build yet. Most of the protocol has been reverse engineered from the Tidepool driver (https://github.com/tidepool-org/chrome-uploader/blob/master/lib/drivers/onetouch/oneTouchVerioIQ.js) with a few assumption brought in from the UltraEasy and Verio 2015. --- glucometerutils/drivers/otverioiq.py | 194 +++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 glucometerutils/drivers/otverioiq.py diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py new file mode 100644 index 0000000..9cd9e16 --- /dev/null +++ b/glucometerutils/drivers/otverioiq.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +"""Driver for LifeScan OneTouch Verio IQ devices. + +Currently work in progress, untested. + +Expected device path: /dev/ttyUSB0 or similar serial port device. Device should +be auto-detected if not provided. +""" + +__author__ = 'Diego Elio Pettenò' +__email__ = 'flameeyes@flameeyes.eu' +__copyright__ = 'Copyright © 2018, Diego Elio Pettenò' +__license__ = 'MIT' + +import binascii +import datetime +import logging + +import construct + +from glucometerutils import common +from glucometerutils.support import construct_extras +from glucometerutils.support import lifescan +from glucometerutils.support import lifescan_binary_protocol +from glucometerutils.support import serial + +_COMMAND_SUCCESS = construct.Const(b'\x04\x06') + +_VERSION_REQUEST = construct.Const(b'\x04\x0d\x02') # Untested + +_VERSION_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'version' / construct.PascalString(construct.Byte, encoding='ascii'), +) + +_SERIAL_NUMBER_REQUEST = construct.Const( + b'\x04\x0b\x00\x02') + +_SERIAL_NUMBER_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'serial_number' / construct.CString(encoding='ascii'), +) + +_READ_RTC_REQUEST = construct.Const(b'\x04\x20\x02') + +_READ_RTC_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, +) + +_WRITE_RTC_REQUEST = construct.Struct( + construct.Const(b'\x04\x20\x01'), + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, +) + +_GLUCOSE_UNIT_REQUEST = construct.Const( + b'\x04\x09\x02\x02') + + +_GLUCOSE_UNIT_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, + construct.Padding(3), +) + +_MEMORY_ERASE_REQUEST = construct.Const(b'\x04\x1a') # Untested + +_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x04\x27\x00') + +_READ_RECORD_COUNT_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'count' / construct.Int16ul, +) + +_READ_RECORD_REQUEST = construct.Struct( + construct.Const(b'\x04\x21'), + 'record_id' / construct.Int16ul, +) + +_READING_RESPONSE = construct.Struct( + _COMMAND_SUCCESS, + 'timestamp' / construct_extras.Timestamp(construct.Int32ul), + 'value' / construct.Int32ul, + 'control' / construct.Byte, # Unknown value +) + + +class Device(serial.SerialDevice): + BAUDRATE = 9600 + DEFAULT_CABLE_ID = '10c4:85a7' # Specific ID for embedded cp210x + TIMEOUT = 0.5 + + def __init__(self, device): + super(Device, self).__init__(device) + self.buffered_reader_ = construct.Rebuffered( + lifescan_binary_protocol.PACKET, tailcutoff=1024) + + def connect(self): + pass + + def disconnect(self): + pass + + def _send_packet(self, message): + pkt = lifescan_binary_protocol.PACKET.build( + {'value': { + 'message': request, + 'link_control': {}, # Verio does not use link_control. + }}) + logging.debug('sending packet: %s', binascii.hexlify(pkt)) + + self.serial_.write(pkt) + self.serial_.flush() + + def _read_packet(self): + raw_pkt = self.buffered_reader_.parse_stream(self.serial_) + logging.debug('received packet: %r', raw_pkt) + + # discard the checksum and copy + pkt = raw_pkt.value + + return pkt + + def _send_request(self, request_format, request_obj, response_format): + try: + request = request_format.build(request_obj) + self._send_packet(request) + + response_pkt = self._read_packet() + + return response_format.parse(response_pkt.message) + except construct.ConstructError as e: + raise lifescan.MalformedCommand(str(e)) + + def get_meter_info(self): + return common.MeterInfo( + 'OneTouch Verio IQ glucometer', + serial_number=self.get_serial_number(), + version_info=( + 'Software version: ' + self.get_version(),), + native_unit=self.get_glucose_unit()) + + def get_version(self): + response = self._send_request( + _VERSION_REQUEST, None, _VERSION_RESPONSE) + + return response.version + + def get_serial_number(self): + response = self._send_request( + _SERIAL_NUMBER_REQUEST, None, _SERIAL_NUMBER_RESPONSE) + + return response.serial_number + + def get_datetime(self): + response = self._send_request( + _READ_RTC_REQUEST, _READ_RTC_RESPONSE) + + return response.timestamp + + def set_datetime(self, date=datetime.datetime.now()): + response = self._send_request( + _WRITE_RTC_REQUEST, { + 'timestamp': date, + }, _READ_RTC_RESPONSE) + + return response.timestamp + + def zero_log(self): + self._send_request(_MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) + + def get_glucose_unit(self): + response = self._send_request( + _GLUCOSE_UNIT_REQUEST, None, _GLUCOSE_UNIT_RESPONSE) + + return response.unit + + def _get_reading_count(self): + response = self._send_request( + _READ_RECORD_REQUEST, {'record_id': _INVALID_RECORD}, + _READING_COUNT_RESPONSE) + return response.count + + def _get_reading(self, record_id): + response = self._send_request( + _READ_RECORD_REQUEST, {'record_id': record_id}, _READING_RESPONSE) + + return common.GlucoseReading( + response.timestamp, float(response.value)) + + def get_readings(self): + record_count = self._get_reading_count() + for record_id in range(record_count): + yield self._get_reading(record_id) From 1aa5295890964b405a25d747d4f8471a7466b8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 21:44:49 +0000 Subject: [PATCH 27/58] otultraeasy: fix AttributeErrors after factoring out link_control struct. --- glucometerutils/drivers/otultraeasy.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index fe7e4ae..aafc2bd 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -137,10 +137,11 @@ def _read_packet(self): # discard the checksum and copy pkt = raw_pkt.value - if not pkt.disconnect and pkt.sequence_number != self.expect_receive_: + if not pkt.link_control.disconnect and ( + pkt.link_control.sequence_number != self.expect_receive_): raise lifescan.MalformedCommand( 'at position 2[0b] expected %02x, received %02x' % ( - self.expect_receive_, pkt.sequence_count)) + self.expect_receive_, pkt.link_connect.sequence_count)) return pkt @@ -149,7 +150,7 @@ def _send_ack(self): def _read_ack(self): pkt = self._read_packet() - assert pkt.acknowledge + assert pkt.link_control.acknowledge def _send_request(self, request_format, request_obj, response_format): try: @@ -160,7 +161,7 @@ def _send_request(self, request_format, request_obj, response_format): self._read_ack() response_pkt = self._read_packet() - assert not response_pkt.acknowledge + assert not response_pkt.link_control.acknowledge self.expect_receive_ = not self.expect_receive_ self._send_ack() From b5784bb35b9968bf977d1319c3301958d671f45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 22:51:43 +0000 Subject: [PATCH 28/58] lifescan binary protocol: make the packet generator a function. This ensures that the command prefix is added directly into the packet structure, simplifying the rest of the code. Per driver changes: - otverio2015: command prefix is changed from 0x04 to 0x03; this ensures that all the responses share the same prefix (READ RECORD COUNT does not repeat the selected command prefix). - otverioiq: command prefix is changed from 0x04 to 0x03, to align with the otverio2015 driver and the trace from OneTouch Diabetes Management Software. --- glucometerutils/drivers/otultraeasy.py | 22 +++---- glucometerutils/drivers/otverio2015.py | 21 ++++--- glucometerutils/drivers/otverioiq.py | 21 ++++--- .../support/lifescan_binary_protocol.py | 57 ++++++++++--------- 4 files changed, 65 insertions(+), 56 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index aafc2bd..1876402 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -30,12 +30,14 @@ from glucometerutils.support import lifescan_binary_protocol from glucometerutils.support import serial +_PACKET = lifescan_binary_protocol.LifeScanPacket( + 0x05, True) _INVALID_RECORD = 501 -_COMMAND_SUCCESS = construct.Const(b'\x05\x06') +_COMMAND_SUCCESS = construct.Const(b'\x06') -_VERSION_REQUEST = construct.Const(b'\x05\x0d\x02') +_VERSION_REQUEST = construct.Const(b'\x0d\x02') _VERSION_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -43,7 +45,7 @@ ) _SERIAL_NUMBER_REQUEST = construct.Const( - b'\x05\x0B\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00') + b'\x0B\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00') _SERIAL_NUMBER_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -51,7 +53,7 @@ ) _DATETIME_REQUEST = construct.Struct( - construct.Const(b'\x05\x20'), # 0x20 is the datetime + construct.Const(b'\x20'), # 0x20 is the datetime 'request_type' / construct.Enum(construct.Byte, write=0x01, read=0x02), 'timestamp' / construct.Default( construct_extras.Timestamp(construct.Int32ul), @@ -64,7 +66,7 @@ ) _GLUCOSE_UNIT_REQUEST = construct.Const( - b'\x05\x09\x02\x09\x00\x00\x00\x00') + b'\x09\x02\x09\x00\x00\x00\x00') _GLUCOSE_UNIT_RESPONSE = construct.Struct( @@ -73,15 +75,15 @@ construct.Padding(3), ) -_MEMORY_ERASE_REQUEST = construct.Const(b'\x05\x1A') +_MEMORY_ERASE_REQUEST = construct.Const(b'\x1A') _READING_COUNT_RESPONSE = construct.Struct( - construct.Const(b'\x05\x0f'), + construct.Const(b'\x0f'), 'count' / construct.Int16ul, ) _READ_RECORD_REQUEST = construct.Struct( - construct.Const(b'\x05\x1f'), + construct.Const(b'\x1f'), 'record_id' / construct.Int16ul, ) @@ -102,7 +104,7 @@ def __init__(self, device): self.sent_counter_ = False self.expect_receive_ = False self.buffered_reader_ = construct.Rebuffered( - lifescan_binary_protocol.PACKET, tailcutoff=1024) + _PACKET, tailcutoff=1024) def connect(self): try: @@ -115,7 +117,7 @@ def disconnect(self): self.connect() def _send_packet(self, message, acknowledge=False, disconnect=False): - pkt = lifescan_binary_protocol.PACKET.build( + pkt = _PACKET.build( {'value': { 'message': message, 'link_control': { diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 9aff4d2..5bc11dc 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -42,12 +42,13 @@ _REGISTER_SIZE = 512 _PACKET = construct.Padded( - _REGISTER_SIZE, construct.Embedded(lifescan_binary_protocol.PACKET)) + _REGISTER_SIZE, construct.Embedded( + lifescan_binary_protocol.LifeScanPacket(0x03, False))) -_COMMAND_SUCCESS = construct.Const(b'\x04\x06') +_COMMAND_SUCCESS = construct.Const(b'\x06') _QUERY_REQUEST = construct.Struct( - construct.Const(b'\x04\xe6\x02'), + construct.Const(b'\xe6\x02'), 'selector' / construct.Enum( construct.Byte, serial=0x00, model=0x01, software=0x02), ) @@ -59,18 +60,17 @@ ) _READ_PARAMETER_REQUEST = construct.Struct( - construct.Const(b'\x04'), 'selector' / construct.Enum( construct.Byte, unit=0x04), ) _READ_UNIT_RESPONSE = construct.Struct( - construct.Const(b'\x03\x06'), # different from _COMMAND_SUCCESS + _COMMAND_SUCCESS, 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) -_READ_RTC_REQUEST = construct.Const(b'\x04\x20\x02') +_READ_RTC_REQUEST = construct.Const(b'\x20\x02') _READ_RTC_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -78,13 +78,13 @@ ) _WRITE_RTC_REQUEST = construct.Struct( - construct.Const(b'\x04\x20\x01'), + construct.Const(b'\x20\x01'), 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) -_MEMORY_ERASE_REQUEST = construct.Const(b'\x04\x1a') +_MEMORY_ERASE_REQUEST = construct.Const(b'\x1a') -_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x04\x27\x00') +_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x27\x00') _READ_RECORD_COUNT_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -92,7 +92,7 @@ ) _READ_RECORD_REQUEST = construct.Struct( - construct.Const(b'\x04\x31\x02'), + construct.Const(b'\x31\x02'), 'record_id' / construct.Int16ul, construct.Const(b'\x00'), ) @@ -161,7 +161,6 @@ def _send_request(self, lba, request_format, request_obj, response_format): request = request_format.build(request_obj) request_raw = _PACKET.build({'value': { 'message': request, - 'link_control': {}, # Verio does not use link_control. }}) logging.debug( 'Request sent: %s', binascii.hexlify(request_raw)) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 9cd9e16..c4e70cf 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -24,9 +24,12 @@ from glucometerutils.support import lifescan_binary_protocol from glucometerutils.support import serial -_COMMAND_SUCCESS = construct.Const(b'\x04\x06') +_PACKET = lifescan_binary_protocol.LifeScanPacket( + 0x03, True) -_VERSION_REQUEST = construct.Const(b'\x04\x0d\x02') # Untested +_COMMAND_SUCCESS = construct.Const(b'\x06') + +_VERSION_REQUEST = construct.Const(b'\x0d\x02') # Untested _VERSION_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -34,14 +37,14 @@ ) _SERIAL_NUMBER_REQUEST = construct.Const( - b'\x04\x0b\x00\x02') + b'\x0b\x00\x02') _SERIAL_NUMBER_RESPONSE = construct.Struct( _COMMAND_SUCCESS, 'serial_number' / construct.CString(encoding='ascii'), ) -_READ_RTC_REQUEST = construct.Const(b'\x04\x20\x02') +_READ_RTC_REQUEST = construct.Const(b'\x20\x02') _READ_RTC_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -49,12 +52,12 @@ ) _WRITE_RTC_REQUEST = construct.Struct( - construct.Const(b'\x04\x20\x01'), + construct.Const(b'\x20\x01'), 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) _GLUCOSE_UNIT_REQUEST = construct.Const( - b'\x04\x09\x02\x02') + b'\x09\x02\x02') _GLUCOSE_UNIT_RESPONSE = construct.Struct( @@ -63,9 +66,9 @@ construct.Padding(3), ) -_MEMORY_ERASE_REQUEST = construct.Const(b'\x04\x1a') # Untested +_MEMORY_ERASE_REQUEST = construct.Const(b'\x1a') # Untested -_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x04\x27\x00') +_READ_RECORD_COUNT_REQUEST = construct.Const(b'\x27\x00') _READ_RECORD_COUNT_RESPONSE = construct.Struct( _COMMAND_SUCCESS, @@ -73,7 +76,7 @@ ) _READ_RECORD_REQUEST = construct.Struct( - construct.Const(b'\x04\x21'), + construct.Const(b'\x21'), 'record_id' / construct.Int16ul, ) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 0143cd4..92a0002 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -18,36 +18,41 @@ from glucometerutils.support import construct_extras from glucometerutils.support import lifescan +_LINK_CONTROL = construct.BitStruct( + construct.Padding(3), + 'more' / construct.Default(construct.Flag, False), + 'disconnect' / construct.Default(construct.Flag, False), + 'acknowledge' / construct.Default(construct.Flag, False), + 'expect_receive' / construct.Default(construct.Flag, False), + 'sequence_number' / construct.Default(construct.Flag, False), +) + +def LifeScanPacket(command_prefix, include_link_control): + if include_link_control: + link_control_construct = _LINK_CONTROL + else: + link_control_construct = construct.Const(b'\x00') -PACKET = construct.Struct( - construct.RawCopy( - construct.Embedded( - construct.Struct( - construct.Const(b'\x02'), # stx - 'length' / construct.Rebuild( - construct.Byte, lambda ctx: len(ctx.message) + 6), - # The following structure is only used by some of the devices. - 'link_control' / construct.BitStruct( - construct.Padding(3), - 'more' / construct.Default( - construct.Flag, False), - 'disconnect' / construct.Default( - construct.Flag, False), - 'acknowledge' / construct.Default( - construct.Flag, False), - 'expect_receive' / construct.Default( - construct.Flag, False), - 'sequence_number' / construct.Default( - construct.Flag, False), + command_prefix_construct = construct.Const(construct.Byte, command_prefix) + + return construct.Struct( + construct.RawCopy( + construct.Embedded( + construct.Struct( + construct.Const(b'\x02'), # stx + 'length' / construct.Rebuild( + construct.Byte, lambda ctx: len(ctx.message) + 7), + 'link_control' / link_control_construct, + 'command_prefix' / command_prefix_construct, + 'message' / construct.Bytes( + length=lambda ctx: ctx.length - 7), + construct.Const(b'\x03'), # etx ), - 'message' / construct.Bytes(length=lambda ctx: ctx.length - 6), - construct.Const(b'\x03'), # etx ), ), - ), - 'checksum' / construct.Checksum( - construct.Int16ul, lifescan.crc_ccitt, construct.this.data), -) + 'checksum' / construct.Checksum( + construct.Int16ul, lifescan.crc_ccitt, construct.this.data), + ) VERIO_TIMESTAMP = construct_extras.Timestamp( construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 From 005fec07bd69e9b086b3c8a8d2f99309e1709fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 22:59:35 +0000 Subject: [PATCH 29/58] otverioiq: update version and serial number requests. These are now coming from the software trace. The version response is funny though. --- glucometerutils/drivers/otverioiq.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index c4e70cf..96b9c93 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -29,15 +29,17 @@ _COMMAND_SUCCESS = construct.Const(b'\x06') -_VERSION_REQUEST = construct.Const(b'\x0d\x02') # Untested +_VERSION_REQUEST = construct.Const(b'\x0d\x01') _VERSION_RESPONSE = construct.Struct( _COMMAND_SUCCESS, 'version' / construct.PascalString(construct.Byte, encoding='ascii'), + # NULL-termination is not included in string length. + construct.Constant('\x00'), ) _SERIAL_NUMBER_REQUEST = construct.Const( - b'\x0b\x00\x02') + b'\x0b\x01\x02') _SERIAL_NUMBER_RESPONSE = construct.Struct( _COMMAND_SUCCESS, From 34b8153d2612b6450650230b596ee51cac82c4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 23:53:28 +0000 Subject: [PATCH 30/58] lifescan binary protocol: factor out _COMMAND_SUCCESS. The success status is always %x06 if there is a message at all. --- glucometerutils/drivers/otultraeasy.py | 16 ++++++++-------- glucometerutils/drivers/otverio2015.py | 17 ++++++++--------- glucometerutils/drivers/otverioiq.py | 18 +++++++++--------- .../support/lifescan_binary_protocol.py | 2 ++ 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 1876402..8e794ca 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -35,12 +35,10 @@ _INVALID_RECORD = 501 -_COMMAND_SUCCESS = construct.Const(b'\x06') - _VERSION_REQUEST = construct.Const(b'\x0d\x02') _VERSION_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'version' / construct.PascalString(construct.Byte, encoding='ascii'), ) @@ -48,7 +46,7 @@ b'\x0B\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00') _SERIAL_NUMBER_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'serial_number' / construct.GreedyString(encoding='ascii'), ) @@ -61,7 +59,7 @@ ) _DATETIME_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / construct_extras.Timestamp(construct.Int32ul), ) @@ -70,7 +68,7 @@ _GLUCOSE_UNIT_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) @@ -88,7 +86,7 @@ ) _READING_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / construct_extras.Timestamp(construct.Int32ul), 'value' / construct.Int32ul, ) @@ -209,7 +207,9 @@ def set_datetime(self, date=datetime.datetime.now()): return response.timestamp def zero_log(self): - self._send_request(_MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) + self._send_request( + _MEMORY_ERASE_REQUEST, None, + lifescan_binary_protocol.COMMAND_SUCCESS) def get_glucose_unit(self): response = self._send_request( diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 5bc11dc..2589a97 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -45,8 +45,6 @@ _REGISTER_SIZE, construct.Embedded( lifescan_binary_protocol.LifeScanPacket(0x03, False))) -_COMMAND_SUCCESS = construct.Const(b'\x06') - _QUERY_REQUEST = construct.Struct( construct.Const(b'\xe6\x02'), 'selector' / construct.Enum( @@ -54,7 +52,7 @@ ) _QUERY_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, # This should be an UTF-16L CString, but construct does not support it. 'value' / construct.GreedyString(encoding='utf-16-le'), ) @@ -65,7 +63,7 @@ ) _READ_UNIT_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) @@ -73,7 +71,7 @@ _READ_RTC_REQUEST = construct.Const(b'\x20\x02') _READ_RTC_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) @@ -87,7 +85,7 @@ _READ_RECORD_COUNT_REQUEST = construct.Const(b'\x27\x00') _READ_RECORD_COUNT_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'count' / construct.Int16ul, ) @@ -104,7 +102,7 @@ } _READ_RECORD_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'inverse_counter' / construct.Int16ul, construct.Padding(1), 'lifetime_counter' / construct.Int16ul, @@ -210,7 +208,7 @@ def get_datetime(self): def set_datetime(self, date=datetime.datetime.now()): self._send_request( 3, _WRITE_RTC_REQUEST, {'timestamp': date}, - _COMMAND_SUCCESS) + lifescan_binary_protocol.COMMAND_SUCCESS) # The device does not return the new datetime, so confirm by calling # READ RTC again. @@ -218,7 +216,8 @@ def set_datetime(self, date=datetime.datetime.now()): def zero_log(self): self._send_request( - 3, _MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) + 3, _MEMORY_ERASE_REQUEST, None, + lifescan_binary_protocol.COMMAND_SUCCESS) def get_glucose_unit(self): response = self._send_request( diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 96b9c93..462832d 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -27,12 +27,10 @@ _PACKET = lifescan_binary_protocol.LifeScanPacket( 0x03, True) -_COMMAND_SUCCESS = construct.Const(b'\x06') - _VERSION_REQUEST = construct.Const(b'\x0d\x01') _VERSION_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'version' / construct.PascalString(construct.Byte, encoding='ascii'), # NULL-termination is not included in string length. construct.Constant('\x00'), @@ -42,14 +40,14 @@ b'\x0b\x01\x02') _SERIAL_NUMBER_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'serial_number' / construct.CString(encoding='ascii'), ) _READ_RTC_REQUEST = construct.Const(b'\x20\x02') _READ_RTC_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, ) @@ -63,7 +61,7 @@ _GLUCOSE_UNIT_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'unit' / lifescan_binary_protocol.GLUCOSE_UNIT, construct.Padding(3), ) @@ -73,7 +71,7 @@ _READ_RECORD_COUNT_REQUEST = construct.Const(b'\x27\x00') _READ_RECORD_COUNT_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'count' / construct.Int16ul, ) @@ -83,7 +81,7 @@ ) _READING_RESPONSE = construct.Struct( - _COMMAND_SUCCESS, + lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / construct_extras.Timestamp(construct.Int32ul), 'value' / construct.Int32ul, 'control' / construct.Byte, # Unknown value @@ -172,7 +170,9 @@ def set_datetime(self, date=datetime.datetime.now()): return response.timestamp def zero_log(self): - self._send_request(_MEMORY_ERASE_REQUEST, None, _COMMAND_SUCCESS) + self._send_request( + _MEMORY_ERASE_REQUEST, None, + lifescan_binary_protocol.COMMAND_SUCCESS) def get_glucose_unit(self): response = self._send_request( diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 92a0002..caa9b63 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -54,6 +54,8 @@ def LifeScanPacket(command_prefix, include_link_control): construct.Int16ul, lifescan.crc_ccitt, construct.this.data), ) +COMMAND_SUCCESS = construct.Const(b'\x06') + VERIO_TIMESTAMP = construct_extras.Timestamp( construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 From 67a70d1a544c76b5352f1dcf297c93599ab307a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 6 Jan 2018 23:54:15 +0000 Subject: [PATCH 31/58] otverioiq: link-control is not used. --- glucometerutils/drivers/otverioiq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 462832d..453ec00 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -25,7 +25,7 @@ from glucometerutils.support import serial _PACKET = lifescan_binary_protocol.LifeScanPacket( - 0x03, True) + 0x03, False) _VERSION_REQUEST = construct.Const(b'\x0d\x01') From 7075275de1b4abcb56ed305500fb799406a8854e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 00:19:16 +0000 Subject: [PATCH 32/58] otverioiq: implement full parsing of the response structure. The TidePool driver does not implement meal comment and it does not validate the full message. I checked the flags with the trace and they match the values in otverio2015. --- glucometerutils/drivers/otverioiq.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 453ec00..f7baa85 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -80,11 +80,21 @@ 'record_id' / construct.Int16ul, ) +_MEAL_FLAG = { + common.Meal.NONE: 0x00, + common.Meal.BEFORE: 0x01, + common.Meal.AFTER: 0x02, +} + _READING_RESPONSE = construct.Struct( lifescan_binary_protocol.COMMAND_SUCCESS, 'timestamp' / construct_extras.Timestamp(construct.Int32ul), 'value' / construct.Int32ul, - 'control' / construct.Byte, # Unknown value + 'control_test' / construct.Flag, + construct.Padding(1), # unknown + 'meal' / construct.SymmetricMapping( + construct.Byte, _MEAL_FLAG), + cosntruct.Padding(2), # unknown ) @@ -190,10 +200,16 @@ def _get_reading(self, record_id): response = self._send_request( _READ_RECORD_REQUEST, {'record_id': record_id}, _READING_RESPONSE) + if response.control_test: + logging.debug('control solution test, ignoring.') + return None + return common.GlucoseReading( - response.timestamp, float(response.value)) + response.timestamp, float(response.value), meal=response.meal) def get_readings(self): record_count = self._get_reading_count() for record_id in range(record_count): - yield self._get_reading(record_id) + reading = self._get_reading(record_id) + if reading: + yield reading From 6467d519702341575a0e3e97706ec51265a84ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 00:23:45 +0000 Subject: [PATCH 33/58] otverioiq: add to the list and to the dependency file. As noted in Issue #30, this is currently untested, but I'm confident it won't take much to get it to working state. --- README | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/README b/README index 2e7a460..74c54de 100644 --- a/README +++ b/README @@ -37,6 +37,7 @@ supported. | LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | | LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | | LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Verio IQ† | `otverioiq` | [construct] [pyserial] | | LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | | LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | | Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | diff --git a/setup.py b/setup.py index 212425a..0a9f258 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ def run_tests(self): 'otultra2': ['pyserial'], 'otultraeasy': ['construct', 'pyserial'], 'otverio2015': ['construct', 'python-scsi'], + 'otverioiq': ['construct', 'pyserial'], 'fsinsulinx': ['construct', 'hidapi'], 'fslibre': ['construct', 'hidapi'], 'fsoptium': ['pyserial'], From 11d8d565ffd67814b96d4e743d67caf5ad882ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 12:00:51 +0000 Subject: [PATCH 34/58] otverioiq: fix typo. --- glucometerutils/drivers/otverioiq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index f7baa85..cf2bc39 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -94,7 +94,7 @@ construct.Padding(1), # unknown 'meal' / construct.SymmetricMapping( construct.Byte, _MEAL_FLAG), - cosntruct.Padding(2), # unknown + construct.Padding(2), # unknown ) From 6459cf5e810d826d558b5be80f83b1d0642cac2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:06:32 +0000 Subject: [PATCH 35/58] otverioiq: fix up syntax. --- glucometerutils/drivers/otverioiq.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index cf2bc39..8cc8051 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -33,7 +33,7 @@ lifescan_binary_protocol.COMMAND_SUCCESS, 'version' / construct.PascalString(construct.Byte, encoding='ascii'), # NULL-termination is not included in string length. - construct.Constant('\x00'), + construct.Const('\x00'), ) _SERIAL_NUMBER_REQUEST = construct.Const( @@ -105,8 +105,7 @@ class Device(serial.SerialDevice): def __init__(self, device): super(Device, self).__init__(device) - self.buffered_reader_ = construct.Rebuffered( - lifescan_binary_protocol.PACKET, tailcutoff=1024) + self.buffered_reader_ = construct.Rebuffered(_PACKET, tailcutoff=1024) def connect(self): pass @@ -115,10 +114,9 @@ def disconnect(self): pass def _send_packet(self, message): - pkt = lifescan_binary_protocol.PACKET.build( + pkt = _PACKET.build( {'value': { - 'message': request, - 'link_control': {}, # Verio does not use link_control. + 'message': message, }}) logging.debug('sending packet: %s', binascii.hexlify(pkt)) From 7752b59a8c036ba1446db782ec1edb195c7064d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:19:20 +0000 Subject: [PATCH 36/58] otverioiq: fix up a couple of syntax errors, and update baud rate. --- glucometerutils/drivers/otverioiq.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 8cc8051..0eb94b4 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -33,7 +33,7 @@ lifescan_binary_protocol.COMMAND_SUCCESS, 'version' / construct.PascalString(construct.Byte, encoding='ascii'), # NULL-termination is not included in string length. - construct.Const('\x00'), + construct.Const(b'\x00'), ) _SERIAL_NUMBER_REQUEST = construct.Const( @@ -99,7 +99,7 @@ class Device(serial.SerialDevice): - BAUDRATE = 9600 + BAUDRATE = 38400 DEFAULT_CABLE_ID = '10c4:85a7' # Specific ID for embedded cp210x TIMEOUT = 0.5 @@ -165,7 +165,7 @@ def get_serial_number(self): def get_datetime(self): response = self._send_request( - _READ_RTC_REQUEST, _READ_RTC_RESPONSE) + _READ_RTC_REQUEST, None, _READ_RTC_RESPONSE) return response.timestamp From 058ddbfe4f860ba0bd5294f996dce0d53290995a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:25:56 +0000 Subject: [PATCH 37/58] otverioiq: fix definition of the reading response, fix get_reading_count method. --- glucometerutils/drivers/otverioiq.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 0eb94b4..d239cff 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -88,10 +88,9 @@ _READING_RESPONSE = construct.Struct( lifescan_binary_protocol.COMMAND_SUCCESS, - 'timestamp' / construct_extras.Timestamp(construct.Int32ul), - 'value' / construct.Int32ul, + 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, + 'value' / construct.Int16ul, 'control_test' / construct.Flag, - construct.Padding(1), # unknown 'meal' / construct.SymmetricMapping( construct.Byte, _MEAL_FLAG), construct.Padding(2), # unknown @@ -190,8 +189,7 @@ def get_glucose_unit(self): def _get_reading_count(self): response = self._send_request( - _READ_RECORD_REQUEST, {'record_id': _INVALID_RECORD}, - _READING_COUNT_RESPONSE) + _READ_RECORD_COUNT_REQUEST, None, _READ_RECORD_COUNT_RESPONSE) return response.count def _get_reading(self, record_id): From d176a98e82fbbb4280cb10c0ffc40bb8476198bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:29:28 +0000 Subject: [PATCH 38/58] otverioiq: fix up set_datetime method. --- glucometerutils/drivers/otverioiq.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index d239cff..d58fa99 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -172,9 +172,11 @@ def set_datetime(self, date=datetime.datetime.now()): response = self._send_request( _WRITE_RTC_REQUEST, { 'timestamp': date, - }, _READ_RTC_RESPONSE) + }, lifescan_binary_protocol.COMMAND_SUCCESS) - return response.timestamp + # The device does not return the new datetime, so confirm by calling + # READ RTC again. + return self.get_datetime() def zero_log(self): self._send_request( From eeb982f39896a66accaf2c253ba78f60cd2cbef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:29:48 +0000 Subject: [PATCH 39/58] otverioiq: zero log is tested, it works. --- glucometerutils/drivers/otverioiq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index d58fa99..9654acb 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -66,7 +66,7 @@ construct.Padding(3), ) -_MEMORY_ERASE_REQUEST = construct.Const(b'\x1a') # Untested +_MEMORY_ERASE_REQUEST = construct.Const(b'\x1a') _READ_RECORD_COUNT_REQUEST = construct.Const(b'\x27\x00') From dc8c15cb4d30f67a661ed8db3755c6dfb205e5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 7 Jan 2018 14:32:08 +0000 Subject: [PATCH 40/58] otverioiq: remove untested marking and list supported features. This closes Issue #30 as I actually tested this and it works perfectly fine. --- README | 2 +- glucometerutils/drivers/otverioiq.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README b/README index 74c54de..4cb7c4c 100644 --- a/README +++ b/README @@ -37,7 +37,7 @@ supported. | LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | | LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | | LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | -| LifeScan | OneTouch Verio IQ† | `otverioiq` | [construct] [pyserial] | +| LifeScan | OneTouch Verio IQ | `otverioiq` | [construct] [pyserial] | | LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | | LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | | Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 9654acb..669900e 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- """Driver for LifeScan OneTouch Verio IQ devices. -Currently work in progress, untested. +Supported features: + - get readings, including pre-/post-meal notes; + - 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. Device will be +auto-detected. -Expected device path: /dev/ttyUSB0 or similar serial port device. Device should -be auto-detected if not provided. """ __author__ = 'Diego Elio Pettenò' From f70de2d32ca9e116b947d412d4632f4e1d961e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 11 Feb 2018 13:43:03 +0000 Subject: [PATCH 41/58] Until Issue #38 is fixed, expect construct 2.8, and not 2.9. The API changed in slightly incompatible ways so I'll have some work to make sure this works on both versions (and that there is some test around it, possibly). --- setup.py | 14 +++++++------- test-requirements.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 0a9f258..046a422 100644 --- a/setup.py +++ b/setup.py @@ -49,15 +49,15 @@ def run_tests(self): # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], - 'otultraeasy': ['construct', 'pyserial'], - 'otverio2015': ['construct', 'python-scsi'], - 'otverioiq': ['construct', 'pyserial'], - 'fsinsulinx': ['construct', 'hidapi'], - 'fslibre': ['construct', 'hidapi'], + 'otultraeasy': ['construct>=2.8,<2.9', 'pyserial'], + 'otverio2015': ['construct>=2.8,<2.9', 'python-scsi'], + 'otverioiq': ['construct>=2.8,<2.9', 'pyserial'], + 'fsinsulinx': ['construct>=2.8,<2.9', 'hidapi'], + 'fslibre': ['construct>=2.8,<2.9', 'hidapi'], 'fsoptium': ['pyserial'], - 'fsprecisionneo': ['construct', 'hidapi'], + 'fsprecisionneo': ['construct>=2.8,<2.9', 'hidapi'], 'accucheck_reports': [], - 'sdcodefree': ['construct', 'pyserial'], + 'sdcodefree': ['construct>=2.8,<2.9', 'pyserial'], }, entry_points = { 'console_scripts': [ diff --git a/test-requirements.txt b/test-requirements.txt index 29ac573..b1dd11d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ absl-py -construct +construct>=2.8,<2.9 pytest pytest-timeout pyserial From 682fd343611c4b5f60611d519c2233f4d4164af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sun, 11 Feb 2018 15:54:28 +0000 Subject: [PATCH 42/58] construct: the construct API is significantly unstable, fix to 2.8.22 only. This still is required to fix Issue #38, but luckily it's just a test failure for now. --- setup.py | 14 +++++++------- test-requirements.txt | 2 +- test/test_construct_extras.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 046a422..c9236fd 100644 --- a/setup.py +++ b/setup.py @@ -49,15 +49,15 @@ def run_tests(self): # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], - 'otultraeasy': ['construct>=2.8,<2.9', 'pyserial'], - 'otverio2015': ['construct>=2.8,<2.9', 'python-scsi'], - 'otverioiq': ['construct>=2.8,<2.9', 'pyserial'], - 'fsinsulinx': ['construct>=2.8,<2.9', 'hidapi'], - 'fslibre': ['construct>=2.8,<2.9', 'hidapi'], + 'otultraeasy': ['construct==2.8.22', 'pyserial'], + 'otverio2015': ['construct==2.8.22', 'python-scsi'], + 'otverioiq': ['construct==2.8.22', 'pyserial'], + 'fsinsulinx': ['construct==2.8.22', 'hidapi'], + 'fslibre': ['construct==2.8.22', 'hidapi'], 'fsoptium': ['pyserial'], - 'fsprecisionneo': ['construct>=2.8,<2.9', 'hidapi'], + 'fsprecisionneo': ['construct==2.8.22', 'hidapi'], 'accucheck_reports': [], - 'sdcodefree': ['construct>=2.8,<2.9', 'pyserial'], + 'sdcodefree': ['construct==2.8.22', 'pyserial'], }, entry_points = { 'console_scripts': [ diff --git a/test-requirements.txt b/test-requirements.txt index b1dd11d..b0ee2ff 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ absl-py -construct>=2.8,<2.9 +construct==2.8.22 pytest pytest-timeout pyserial diff --git a/test/test_construct_extras.py b/test/test_construct_extras.py index 66da5b7..b0cd1c3 100644 --- a/test/test_construct_extras.py +++ b/test/test_construct_extras.py @@ -51,7 +51,7 @@ def test_parse_custom_epoch(self): _TEST_DATE2) def test_build_custom_epoch_negative_failure(self): - with self.assertRaises(construct.core.FieldError): + with self.assertRaises(construct.core.FormatFieldError): construct_extras.Timestamp( construct.Int32ul, epoch=_NEW_EPOCH).build(_TEST_DATE1) From 4819131ff81532c06c5eccc4c70135e72ca3f7c9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Fri, 16 Feb 2018 08:24:25 +0100 Subject: [PATCH 43/58] construct-code is (almost) uptodate --- glucometerutils/drivers/sdcodefree.py | 9 +++++---- glucometerutils/support/construct_extras.py | 4 ++-- glucometerutils/support/freestyle.py | 2 +- .../support/lifescan_binary_protocol.py | 2 +- setup.py | 14 +++++++------- test-requirements.txt | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/glucometerutils/drivers/sdcodefree.py b/glucometerutils/drivers/sdcodefree.py index 2d145cc..55c0bf1 100644 --- a/glucometerutils/drivers/sdcodefree.py +++ b/glucometerutils/drivers/sdcodefree.py @@ -40,7 +40,7 @@ class Direction(enum.Enum): Out = 0x10 _PACKET = construct.Struct( - 'stx' / construct.Const(construct.Byte, 0x53), + 'stx' / construct.Const(0x53, construct.Byte), 'direction' / construct.SymmetricMapping( construct.Byte, {e: e.value for e in Direction}), @@ -49,13 +49,14 @@ class Direction(enum.Enum): 'message' / construct.Bytes(length=lambda ctx: ctx.length - 2), 'checksum' / construct.Checksum( construct.Byte, xor_checksum, construct.this.message), - 'etx' / construct.Const(construct.Byte, 0xAA) + 'etx' / construct.Const(0xAA, construct.Byte) ) _FIRST_MESSAGE = construct.Struct( - construct.Const(construct.Byte, 0x30), + construct.Const(0x30, construct.Byte), 'count' / construct.Int16ub, - construct.Const(construct.Byte, 0xAA)[19]) + construct.Const(0xAA, construct.Byte)[19], +) _CHALLENGE_PACKET_FULL = b'\x53\x20\x04\x10\x30\x20\xAA' _RESPONSE_MESSAGE = b'\x10\x40' diff --git a/glucometerutils/support/construct_extras.py b/glucometerutils/support/construct_extras.py index cb42105..6d4e302 100644 --- a/glucometerutils/support/construct_extras.py +++ b/glucometerutils/support/construct_extras.py @@ -23,11 +23,11 @@ def __init__(self, subcon, epoch=0): super(Timestamp, self).__init__(subcon) self.epoch = epoch - def _encode(self, obj, context): + def _encode(self, obj, context, path): assert isinstance(obj, datetime.datetime) epoch_date = datetime.datetime.utcfromtimestamp(self.epoch) delta = obj - epoch_date return int(delta.total_seconds()) - def _decode(self, obj, context): + def _decode(self, obj, context, path): return datetime.datetime.utcfromtimestamp(obj + self.epoch) diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py index 50f5319..9b7d72e 100644 --- a/glucometerutils/support/freestyle.py +++ b/glucometerutils/support/freestyle.py @@ -26,7 +26,7 @@ _INIT_SEQUENCE = (0x04, 0x05, 0x15, 0x01) _FREESTYLE_MESSAGE = construct.Struct( - 'hid_report' / construct.Const(construct.Byte, 0), + 'hid_report' / construct.Const(0, construct.Byte), 'message_type' / construct.Byte, 'command' / construct.Padded( 63, # command can only be up to 62 bytes, but one is used for length. diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index caa9b63..6579ab0 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -33,7 +33,7 @@ def LifeScanPacket(command_prefix, include_link_control): else: link_control_construct = construct.Const(b'\x00') - command_prefix_construct = construct.Const(construct.Byte, command_prefix) + command_prefix_construct = construct.Const(command_prefix, construct.Byte) return construct.Struct( construct.RawCopy( diff --git a/setup.py b/setup.py index c9236fd..0a9f258 100644 --- a/setup.py +++ b/setup.py @@ -49,15 +49,15 @@ def run_tests(self): # These are all the drivers' dependencies. Optional dependencies are # listed as mandatory for the feature. 'otultra2': ['pyserial'], - 'otultraeasy': ['construct==2.8.22', 'pyserial'], - 'otverio2015': ['construct==2.8.22', 'python-scsi'], - 'otverioiq': ['construct==2.8.22', 'pyserial'], - 'fsinsulinx': ['construct==2.8.22', 'hidapi'], - 'fslibre': ['construct==2.8.22', 'hidapi'], + 'otultraeasy': ['construct', 'pyserial'], + 'otverio2015': ['construct', 'python-scsi'], + 'otverioiq': ['construct', 'pyserial'], + 'fsinsulinx': ['construct', 'hidapi'], + 'fslibre': ['construct', 'hidapi'], 'fsoptium': ['pyserial'], - 'fsprecisionneo': ['construct==2.8.22', 'hidapi'], + 'fsprecisionneo': ['construct', 'hidapi'], 'accucheck_reports': [], - 'sdcodefree': ['construct==2.8.22', 'pyserial'], + 'sdcodefree': ['construct', 'pyserial'], }, entry_points = { 'console_scripts': [ diff --git a/test-requirements.txt b/test-requirements.txt index b0ee2ff..29ac573 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ absl-py -construct==2.8.22 +construct pytest pytest-timeout pyserial From 9d3e0ee1fb2dd2db8d838723316c8329d049b51d Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Fri, 16 Feb 2018 08:51:39 +0100 Subject: [PATCH 44/58] construct code fixed improper embedding --- glucometerutils/drivers/otultraeasy.py | 6 +++--- glucometerutils/drivers/otverio2015.py | 10 +++++----- glucometerutils/drivers/otverioiq.py | 6 +++--- glucometerutils/support/lifescan_binary_protocol.py | 6 ++---- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/glucometerutils/drivers/otultraeasy.py b/glucometerutils/drivers/otultraeasy.py index 8e794ca..f0e16a7 100644 --- a/glucometerutils/drivers/otultraeasy.py +++ b/glucometerutils/drivers/otultraeasy.py @@ -116,7 +116,7 @@ def disconnect(self): def _send_packet(self, message, acknowledge=False, disconnect=False): pkt = _PACKET.build( - {'value': { + {'data': {'value': { 'message': message, 'link_control': { 'sequence_number': self.sent_counter_, @@ -124,14 +124,14 @@ def _send_packet(self, message, acknowledge=False, disconnect=False): 'acknowledge': acknowledge, 'disconnect': disconnect, }, - }}) + }}}) logging.debug('sending packet: %s', binascii.hexlify(pkt)) self.serial_.write(pkt) self.serial_.flush() def _read_packet(self): - raw_pkt = self.buffered_reader_.parse_stream(self.serial_) + raw_pkt = self.buffered_reader_.parse_stream(self.serial_).data logging.debug('received packet: %r', raw_pkt) # discard the checksum and copy diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 2589a97..833ad43 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -42,8 +42,8 @@ _REGISTER_SIZE = 512 _PACKET = construct.Padded( - _REGISTER_SIZE, construct.Embedded( - lifescan_binary_protocol.LifeScanPacket(0x03, False))) + _REGISTER_SIZE, + lifescan_binary_protocol.LifeScanPacket(0x03, False)) _QUERY_REQUEST = construct.Struct( construct.Const(b'\xe6\x02'), @@ -157,9 +157,9 @@ def _send_request(self, lba, request_format, request_obj, response_format): """ try: request = request_format.build(request_obj) - request_raw = _PACKET.build({'value': { + request_raw = _PACKET.build({'data': {'value': { 'message': request, - }}) + }}}) logging.debug( 'Request sent: %s', binascii.hexlify(request_raw)) self.scsi_.write10(lba, 1, request_raw) @@ -167,7 +167,7 @@ def _send_request(self, lba, request_format, request_obj, response_format): response_raw = self.scsi_.read10(lba, 1) logging.debug( 'Response received: %s', binascii.hexlify(response_raw.datain)) - response_pkt = _PACKET.parse(response_raw.datain) + response_pkt = _PACKET.parse(response_raw.datain).data logging.debug('Response packet: %r', response_pkt) response = response_format.parse(response_pkt.value.message) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index 669900e..cecac4e 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -120,16 +120,16 @@ def disconnect(self): def _send_packet(self, message): pkt = _PACKET.build( - {'value': { + {'data': {'value': { 'message': message, - }}) + }}}) logging.debug('sending packet: %s', binascii.hexlify(pkt)) self.serial_.write(pkt) self.serial_.flush() def _read_packet(self): - raw_pkt = self.buffered_reader_.parse_stream(self.serial_) + raw_pkt = self.buffered_reader_.parse_stream(self.serial_).data logging.debug('received packet: %r', raw_pkt) # discard the checksum and copy diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 6579ab0..22e96d9 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -36,8 +36,7 @@ def LifeScanPacket(command_prefix, include_link_control): command_prefix_construct = construct.Const(command_prefix, construct.Byte) return construct.Struct( - construct.RawCopy( - construct.Embedded( + 'data' / construct.RawCopy( construct.Struct( construct.Const(b'\x02'), # stx 'length' / construct.Rebuild( @@ -48,10 +47,9 @@ def LifeScanPacket(command_prefix, include_link_control): length=lambda ctx: ctx.length - 7), construct.Const(b'\x03'), # etx ), - ), ), 'checksum' / construct.Checksum( - construct.Int16ul, lifescan.crc_ccitt, construct.this.data), + construct.Int16ul, lifescan.crc_ccitt, construct.this.data.data), ) COMMAND_SUCCESS = construct.Const(b'\x06') From 38d8d3af0ff3af99a1feb91d43c18e869a407a11 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Sat, 17 Feb 2018 15:01:12 +0100 Subject: [PATCH 45/58] corected timestamp comment --- glucometerutils/support/lifescan_binary_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 22e96d9..7b1fbec 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -55,7 +55,7 @@ def LifeScanPacket(command_prefix, include_link_control): COMMAND_SUCCESS = construct.Const(b'\x06') VERIO_TIMESTAMP = construct_extras.Timestamp( - construct.Int32ul, epoch=946684800) # 2010-01-01 00:00 + construct.Int32ul, epoch=946684800) # 2000-01-01 (not 2010) _GLUCOSE_UNIT_MAPPING_TABLE = { common.Unit.MG_DL: 0x00, From 5f8a6fcd22c2e0f5e3811f299247d17a97377084 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Sun, 18 Feb 2018 13:44:05 +0100 Subject: [PATCH 46/58] SymmetricMapping was renamed to Mapping --- glucometerutils/drivers/otverio2015.py | 2 +- glucometerutils/drivers/otverioiq.py | 2 +- glucometerutils/drivers/sdcodefree.py | 4 ++-- glucometerutils/support/lifescan_binary_protocol.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 833ad43..1fe630a 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -108,7 +108,7 @@ 'lifetime_counter' / construct.Int16ul, 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, 'value' / construct.Int16ul, - 'meal' / construct.SymmetricMapping( + 'meal' / construct.Mapping( construct.Byte, _MEAL_FLAG), construct.Padding(4), ) diff --git a/glucometerutils/drivers/otverioiq.py b/glucometerutils/drivers/otverioiq.py index cecac4e..ead19a9 100644 --- a/glucometerutils/drivers/otverioiq.py +++ b/glucometerutils/drivers/otverioiq.py @@ -97,7 +97,7 @@ 'timestamp' / lifescan_binary_protocol.VERIO_TIMESTAMP, 'value' / construct.Int16ul, 'control_test' / construct.Flag, - 'meal' / construct.SymmetricMapping( + 'meal' / construct.Mapping( construct.Byte, _MEAL_FLAG), construct.Padding(2), # unknown ) diff --git a/glucometerutils/drivers/sdcodefree.py b/glucometerutils/drivers/sdcodefree.py index 55c0bf1..5bc76fb 100644 --- a/glucometerutils/drivers/sdcodefree.py +++ b/glucometerutils/drivers/sdcodefree.py @@ -41,7 +41,7 @@ class Direction(enum.Enum): _PACKET = construct.Struct( 'stx' / construct.Const(0x53, construct.Byte), - 'direction' / construct.SymmetricMapping( + 'direction' / construct.Mapping( construct.Byte, {e: e.value for e in Direction}), 'length' / construct.Rebuild( @@ -82,7 +82,7 @@ class Direction(enum.Enum): 'hour' / construct.Byte, 'minute' / construct.Byte, 'value' / construct.Int16ub, - 'meal' / construct.SymmetricMapping( + 'meal' / construct.Mapping( construct.Byte, _MEAL_FLAG), construct.Byte[7], ) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 7b1fbec..9bdb220 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -62,5 +62,5 @@ def LifeScanPacket(command_prefix, include_link_control): common.Unit.MMOL_L: 0x01, } -GLUCOSE_UNIT = construct.SymmetricMapping( +GLUCOSE_UNIT = construct.Mapping( construct.Byte, _GLUCOSE_UNIT_MAPPING_TABLE) From 8287a1bd710e1d4a37ca5a3921ed2349befb404d Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Wed, 7 Mar 2018 02:02:55 +0100 Subject: [PATCH 47/58] CString supports UTF-16/32-LE/BE --- glucometerutils/drivers/otverio2015.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/glucometerutils/drivers/otverio2015.py b/glucometerutils/drivers/otverio2015.py index 1fe630a..a7cadf5 100644 --- a/glucometerutils/drivers/otverio2015.py +++ b/glucometerutils/drivers/otverio2015.py @@ -53,8 +53,7 @@ _QUERY_RESPONSE = construct.Struct( lifescan_binary_protocol.COMMAND_SUCCESS, - # This should be an UTF-16L CString, but construct does not support it. - 'value' / construct.GreedyString(encoding='utf-16-le'), + 'value' / construct.CString(encoding='utf-16-le'), ) _READ_PARAMETER_REQUEST = construct.Struct( From 8fe51bcb2d208654aefba38004aaa105a6e8339a Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Wed, 7 Mar 2018 02:09:20 +0100 Subject: [PATCH 48/58] Update sdcodefree.py lambdafied this expression --- glucometerutils/drivers/sdcodefree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glucometerutils/drivers/sdcodefree.py b/glucometerutils/drivers/sdcodefree.py index 5bc76fb..ac3e228 100644 --- a/glucometerutils/drivers/sdcodefree.py +++ b/glucometerutils/drivers/sdcodefree.py @@ -45,8 +45,8 @@ class Direction(enum.Enum): construct.Byte, {e: e.value for e in Direction}), 'length' / construct.Rebuild( - construct.Byte, lambda ctx: len(ctx.message) + 2), - 'message' / construct.Bytes(length=lambda ctx: ctx.length - 2), + construct.Byte, lambda this: len(this.message) + 2), + 'message' / construct.Bytes(lambda this: len(this.message)), 'checksum' / construct.Checksum( construct.Byte, xor_checksum, construct.this.message), 'etx' / construct.Const(0xAA, construct.Byte) From 84aad729d78f53d6acbac283b3a3a9626c767754 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bulski Date: Wed, 7 Mar 2018 02:15:05 +0100 Subject: [PATCH 49/58] Update lifescan_binary_protocol.py lambdafied this expression --- glucometerutils/support/lifescan_binary_protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 9bdb220..1771dcf 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -40,11 +40,11 @@ def LifeScanPacket(command_prefix, include_link_control): construct.Struct( construct.Const(b'\x02'), # stx 'length' / construct.Rebuild( - construct.Byte, lambda ctx: len(ctx.message) + 7), + construct.Byte, lambda this: len(this.message) + 7), 'link_control' / link_control_construct, 'command_prefix' / command_prefix_construct, 'message' / construct.Bytes( - length=lambda ctx: ctx.length - 7), + lambda this: len(this.message)), construct.Const(b'\x03'), # etx ), ), From 8cc54e346a649effe117f8b684a6b9a5693eed1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 17 Mar 2018 13:38:14 +0000 Subject: [PATCH 50/58] fsoptium: add debug logging when sending commands. --- glucometerutils/drivers/fsoptium.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/glucometerutils/drivers/fsoptium.py b/glucometerutils/drivers/fsoptium.py index 5c57ae3..1e1a319 100644 --- a/glucometerutils/drivers/fsoptium.py +++ b/glucometerutils/drivers/fsoptium.py @@ -90,11 +90,15 @@ class Device(serial.SerialDevice): def _send_command(self, command): cmd_bytes = bytes('$%s\r\n' % command, 'ascii') + logging.debug('Sending command: %r', cmd_bytes) + self.serial_.write(cmd_bytes) self.serial_.flush() response = self.serial_.readlines() + logging.debug('Received response: %r', response) + # 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') From 45f68d03bceedbb23c64e7c4457297ceb09b61d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Sat, 17 Mar 2018 13:49:27 +0000 Subject: [PATCH 51/58] freestyle support: add debug logging of commands sent and received. --- glucometerutils/support/freestyle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py index 9b7d72e..7f9c7e8 100644 --- a/glucometerutils/support/freestyle.py +++ b/glucometerutils/support/freestyle.py @@ -103,12 +103,16 @@ def _send_command(self, message_type, command): usb_packet = _FREESTYLE_MESSAGE.build( {'message_type': message_type, 'command': command}) + logging.debug('Sending packet: %r', usb_packet) + self._write(usb_packet) def _read_response(self): """Read the response from the device and extracts it.""" usb_packet = self._read() + logging.debug('Read packet: %r', usb_packet) + assert usb_packet message_type = usb_packet[0] message_length = usb_packet[1] From 83466bf0faa68aec9466b850a5d925fe483abca5 Mon Sep 17 00:00:00 2001 From: Naokazu Terada Date: Sat, 14 Apr 2018 15:11:38 +0900 Subject: [PATCH 52/58] Add double quotes according to @arvchristos suggestion on 'Example Usage' section --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 4cb7c4c..0d90ea2 100644 --- a/README +++ b/README @@ -23,7 +23,7 @@ be able to set this up using `virtualenv` and `pip`: $ python3 -m venv $(pwd)/glucometerutils-venv $ . glucometerutils-venv/bin/activate (glucometerutils-venv) $ DRIVER=myglucometer-driver # see table below -(glucometerutils-venv) $ pip install git+https://github.com/Flameeyes/glucometerutils.git#egg=project[${DRIVER}] +(glucometerutils-venv) $ pip install "git+https://github.com/Flameeyes/glucometerutils.git#egg=project[${DRIVER}]" (glucometerutils-venv) $ glucometer --driver ${DRIVER} help ``` From eee15bab2a7c2c80e240c81b139c40ae17f41b16 Mon Sep 17 00:00:00 2001 From: "Wesley T. Honeycutt" Date: Tue, 22 May 2018 16:38:46 -0500 Subject: [PATCH 53/58] Fix for non-integer errors This is a quick fix I used to address an error for "HI" readings on my unit. This comes up when testing inhuman blood. There may be a "LO", but I have not encountered it, so I do not know how the specifics of it. This error may come up on other units, but I have just done something about the hardware I have access to. --- glucometerutils/drivers/fsprecisionneo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/glucometerutils/drivers/fsprecisionneo.py b/glucometerutils/drivers/fsprecisionneo.py index cb58263..296c943 100644 --- a/glucometerutils/drivers/fsprecisionneo.py +++ b/glucometerutils/drivers/fsprecisionneo.py @@ -81,7 +81,11 @@ def get_readings(self): # Build a _reading object by parsing each of the entries in the raw # record - values = [int(v) for v in record] + values = [] + for v in record: + if v == "HI": + v = 999 + values.append(int(v)) raw_reading = _NeoReading._make(values[:len(_NeoReading._fields)]) timestamp = datetime.datetime( From 7b000e080a553a66d72b561887bdcec55ddd5ce9 Mon Sep 17 00:00:00 2001 From: "Wesley T. Honeycutt" Date: Tue, 22 May 2018 17:35:46 -0500 Subject: [PATCH 54/58] change 999 to inf --- glucometerutils/drivers/fsprecisionneo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/drivers/fsprecisionneo.py b/glucometerutils/drivers/fsprecisionneo.py index 296c943..1153908 100644 --- a/glucometerutils/drivers/fsprecisionneo.py +++ b/glucometerutils/drivers/fsprecisionneo.py @@ -84,7 +84,7 @@ def get_readings(self): values = [] for v in record: if v == "HI": - v = 999 + v = float("inf") values.append(int(v)) raw_reading = _NeoReading._make(values[:len(_NeoReading._fields)]) From fbeef8ab4f4d19ae048a6460fcdc3c5782e2b132 Mon Sep 17 00:00:00 2001 From: Noel Cragg Date: Sun, 8 Jul 2018 06:12:15 +0000 Subject: [PATCH 55/58] fix invalid self-reference During the structure rebuild, the 'message' key is removed before its callback is invoked, causing 'this.message' to raise a nonexistent key error. This change reverts the line in question to its pre-84aad729 logic (but leaving the variable name substitutions in place). --- glucometerutils/support/lifescan_binary_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/support/lifescan_binary_protocol.py b/glucometerutils/support/lifescan_binary_protocol.py index 1771dcf..4632a94 100644 --- a/glucometerutils/support/lifescan_binary_protocol.py +++ b/glucometerutils/support/lifescan_binary_protocol.py @@ -44,7 +44,7 @@ def LifeScanPacket(command_prefix, include_link_control): 'link_control' / link_control_construct, 'command_prefix' / command_prefix_construct, 'message' / construct.Bytes( - lambda this: len(this.message)), + lambda this: this.length - 7), construct.Const(b'\x03'), # etx ), ), From f956feec452cd7c2a3287740173fd20414fe9ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Tue, 24 Jul 2018 22:16:28 +0100 Subject: [PATCH 56/58] Fix sdcodefree driver, the same as the lifescan changes. Thanks to Noel Cragg for reporting this. --- glucometerutils/drivers/sdcodefree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glucometerutils/drivers/sdcodefree.py b/glucometerutils/drivers/sdcodefree.py index ac3e228..4636d2a 100644 --- a/glucometerutils/drivers/sdcodefree.py +++ b/glucometerutils/drivers/sdcodefree.py @@ -46,7 +46,7 @@ class Direction(enum.Enum): {e: e.value for e in Direction}), 'length' / construct.Rebuild( construct.Byte, lambda this: len(this.message) + 2), - 'message' / construct.Bytes(lambda this: len(this.message)), + 'message' / construct.Bytes(lambda this: this.length - 2), 'checksum' / construct.Checksum( construct.Byte, xor_checksum, construct.this.message), 'etx' / construct.Const(0xAA, construct.Byte) From 75d8f0746a1b6f11b51a66f46f3ece879a59ea2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Tue, 24 Jul 2018 22:43:15 +0100 Subject: [PATCH 57/58] test-requirements: add some minimum version specifications. --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 29ac573..9a9c847 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ absl-py -construct -pytest -pytest-timeout +construct>=2.9 +pytest>=3.6.0 +pytest-timeout>=1.3.0 pyserial From 8b7665805ad4480f87fbdec20cafff146dadd83e Mon Sep 17 00:00:00 2001 From: garberw <45077264+garberw@users.noreply.github.com> Date: Thu, 15 Nov 2018 10:45:10 -0800 Subject: [PATCH 58/58] Add files via upload --- glucometerutils/drivers/fslite.py | 369 ++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 glucometerutils/drivers/fslite.py diff --git a/glucometerutils/drivers/fslite.py b/glucometerutils/drivers/fslite.py new file mode 100644 index 0000000..fd4a461 --- /dev/null +++ b/glucometerutils/drivers/fslite.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- +"""Driver for FreeStyle Lite devices. + +Supported features: + - get readings (ignores ketone results); + - use the glucose unit preset on the device by default; + - get and set date and time; + - get serial number and software version. + +Expected device path: /dev/ttyUSB0 or similar serial port device. + +Further information on the device protocol can be found at + +http://www.flupzor.nl/protocol.html +""" + +__author__ = 'Diego Elio Pettenò, William Garber' +__email__ = 'flameeyes@flameeyes.eu, william.garber@att.net' +__copyright__ = 'Copyright © 2016-2017, Diego Elio Pettenò' +__license__ = 'MIT' + +import datetime +import logging +import re + +from glucometerutils import common +from glucometerutils import exceptions +from glucometerutils.support import serial + + +_CLOCK_RE = re.compile( + r'^(?P[A-Z][a-z]{2}) (?P[0-9]{2}) (?P[0-9]{4}) ' + r'(?P