Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ supported.
| GlucoRx | NexusQ | `td42xx` | [construct] [pyserial]² [hidapi] |
| Menarini | GlucoMen Nexus | `td42xx` | [construct] [pyserial]² [hidapi] |
| Aktivmed | GlucoCheck XL | `td42xx` | [construct] [pyserial]² [hidapi] |
| Ascensia | Contour Next | `contournext` | [construct] [hidapi]‡ |
| Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ |
| Menarini | GlucoMen areo³ | `glucomenareo` | [pyserial] [crcmod] |

Expand Down
85 changes: 85 additions & 0 deletions glucometerutils/drivers/contournext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# SPDX-FileCopyrightText: © 2026 The glucometerutils Authors
# SPDX-License-Identifier: MIT
"""Driver for Contour Next devices.

Supported features:
- get readings (blood glucose), including comments;
- get date and time;
- get serial number and software version;
- get device info (e.g. unit)

Expected device path: /dev/hidraw4 or similar HID device. Optional when using
HIDAPI.

Further information on the device protocol can be found at

http://protocols.ascensia.com/Programming-Guide.aspx

"""

import datetime
from collections.abc import Generator
from typing import NoReturn, Optional

from glucometerutils import common
from glucometerutils.support import contourusb


def _extract_timestamp(parsed_record: dict[str, str]):
"""Extract the timestamp from a parsed record.

This leverages the fact that all the reading records have the same base structure.
"""
# Pad with zeros for missing time components
datetime_str = parsed_record["datetime"].ljust(14, '0')
return datetime.datetime.strptime(datetime_str, '%Y%m%d%H%M%S')


class Device(contourusb.ContourHidDevice):
"""Glucometer driver for Contour Next devices."""

def __init__(self, device: Optional[str]) -> None:
super().__init__(
(0x1A79, 0x7900),
device,
header_record_re=contourusb._HEADER_RECORD_RE_NEXT,
)

def get_meter_info(self) -> common.MeterInfo:
self._get_info_record()
return common.MeterInfo(
"Contour Next",
serial_number=self._get_serial_number(),
version_info=("Meter versions: " + self._get_version(),),
native_unit=self.get_glucose_unit(),
)

def get_glucose_unit(self) -> common.Unit:
if self._get_glucose_unit() == "0":
return common.Unit.MG_DL
else:
return common.Unit.MMOL_L

def get_readings(self) -> Generator[common.AnyReading, None, None]:
"""
Get reading dump from download data mode(all readings stored)
This meter supports only blood samples
"""
for parsed_record in self._get_multirecord():
yield common.GlucoseReading(
_extract_timestamp(parsed_record),
int(parsed_record["value"]),
comment=parsed_record["markers"],
measure_method=common.MeasurementMethod.BLOOD_SAMPLE,
)

def get_serial_number(self) -> NoReturn:
raise NotImplementedError

def _set_device_datetime(self, date: datetime.datetime) -> NoReturn:
raise NotImplementedError

def zero_log(self) -> NoReturn:
raise NotImplementedError
21 changes: 9 additions & 12 deletions glucometerutils/drivers/contourusb.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,20 @@ def _extract_timestamp(parsed_record: dict[str, str]):

This leverages the fact that all the reading records have the same base structure.
"""
datetime_str = parsed_record["datetime"]

return datetime.datetime(
int(datetime_str[0:4]), # year
int(datetime_str[4:6]), # month
int(datetime_str[6:8]), # day
int(datetime_str[8:10]), # hour
int(datetime_str[10:12]), # minute
0,
)
# Pad with zeros for missing time components
datetime_str = parsed_record["datetime"].ljust(14, '0')
return datetime.datetime.strptime(datetime_str, '%Y%m%d%H%M%S')


class Device(contourusb.ContourHidDevice):
"""Glucometer driver for Contour devices."""
"""Glucometer driver for Contour USB devices."""

def __init__(self, device: Optional[str]) -> None:
super().__init__((0x1A79, 0x6002), device)
super().__init__(
(0x1A79, 0x6002),
device,
header_record_re=contourusb._HEADER_RECORD_RE_USB,
)

def get_meter_info(self) -> common.MeterInfo:
self._get_info_record()
Expand Down
95 changes: 95 additions & 0 deletions glucometerutils/drivers/tests/test_contournext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SPDX-FileCopyrightText: 2026 The glucometerutils Authors
#
# SPDX-License-Identifier: MIT

"""Tests for the Contour Next protocol support."""

# pylint: disable=protected-access,missing-docstring

import datetime
from unittest.mock import Mock

from absl.testing import absltest

from glucometerutils.support import contourusb


class TestContourNext(absltest.TestCase):
header_record = b"\x04\x021H|\\^&||0t4cvJ|Contour7900^02.13\\01.00\\02.40^7901H33A1578|A=0^C=6^R=0^S=0^U=0^V=10600^X=070070180130^a=0^J=0|25|||||P|1|20260208104218|\r\x171A\r\n\x05"

def setUp(self):
super().setUp()
self.mock_dev = Mock()
self.mock_dev._header_record_re = contourusb._HEADER_RECORD_RE_NEXT

def test_get_datetime(self):
self.mock_dev.datetime = "20260208104218"
self.assertEqual(
datetime.datetime(2026, 2, 8, 10, 42, 18),
contourusb.ContourHidDevice.get_datetime(self.mock_dev),
)

def test_RECORD_FORMAT_match(self):
header_record_decoded = self.header_record.decode()
stx = header_record_decoded.find("\x02")

result = contourusb._RECORD_FORMAT.match(header_record_decoded[stx:]).group("text")

self.assertEqual(
"H|\\^&||0t4cvJ|Contour7900^02.13\\01.00\\02.40^7901H33A1578|A=0^C=6^R=0^S=0^U=0^V=10600^X=070070180130^a=0^J=0|25|||||P|1|20260208104218|",
result,
)

def test_parse_header_record(self):
header_record_decoded = self.header_record.decode()
stx = header_record_decoded.find("\x02")

result = contourusb._RECORD_FORMAT.match(header_record_decoded[stx:]).group("text")
contourusb.ContourHidDevice.parse_header_record(self.mock_dev, result)

self.assertEqual(self.mock_dev.field_del, "\\")
self.assertEqual(self.mock_dev.repeat_del, "^")
self.assertEqual(self.mock_dev.component_del, "&")
self.assertEqual(self.mock_dev.escape_del, "|")

self.assertEqual(self.mock_dev.product_code, "Contour7900")

self.assertEqual(self.mock_dev.dig_ver, "02.13")
self.assertEqual(self.mock_dev.anlg_ver, "01.00")
self.assertEqual(self.mock_dev.agp_ver, "02.40")
self.assertEqual(self.mock_dev.serial_num, "7901H33A1578")

self.assertEqual(self.mock_dev.res_marking, "0")
self.assertEqual(self.mock_dev.config_bits, "6")

self.assertEqual(self.mock_dev.ref_method, "0")
self.assertEqual(self.mock_dev.internal, "0")
self.assertEqual(self.mock_dev.unit, "0")
self.assertEqual(self.mock_dev.lo_bound, "10")
self.assertEqual(self.mock_dev.hi_bound, "600")

self.assertEqual(self.mock_dev.post_food_low, "070")
self.assertEqual(self.mock_dev.pre_food_low, "070")

self.assertEqual(self.mock_dev.post_food_high, "180")
self.assertEqual(self.mock_dev.pre_food_high, "130")

self.assertEqual(self.mock_dev.total, "25")
self.assertEqual(self.mock_dev.spec_ver, "1")

self.assertEqual(self.mock_dev.datetime, "20260208104218")

def test_parse_result_record(self):
result_record = "R|3|^^^Glucose|126|mg/dL^P||T0||20260125085519"
result_dict = contourusb.ContourHidDevice.parse_result_record(
self.mock_dev, result_record
)

self.assertEqual(result_dict["record_type"], "R")
self.assertEqual(result_dict["seq_num"], "3")
self.assertEqual(result_dict["test_id"], "Glucose")
self.assertEqual(result_dict["value"], "126")
self.assertEqual(result_dict["unit"], "mg/dL")
self.assertEqual(result_dict["ref_method"], "P")
self.assertEqual(result_dict["markers"], "T0")
self.assertEqual(result_dict["datetime"], "20260125085519")
15 changes: 10 additions & 5 deletions glucometerutils/drivers/tests/test_contourusb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@
class TestContourUSB(absltest.TestCase):
header_record = b"\x04\x021H|\\^&||7w3LBL|Bayer7390^01.24\\01.04\\09.02.20^7390-2336773^7403-|A=1^C=63^G=1^I=0200^R=0^S=1^U=0^V=10600^X=070070070070180130150250^Y=360126090050099050300089^Z=1|1714||||||1|201909221304\r\x17D7\r\n\x05"

mock_dev = Mock()
def setUp(self):
super().setUp()
self.mock_dev = Mock()
self.mock_dev._header_record_re = contourusb._HEADER_RECORD_RE_USB

def test_get_datetime(self):
import datetime

self.datetime = "201908071315" # returned by
# datetime is padded to 14 chars (YYYYMMDDHHMMSS) by parse_header_record
self.mock_dev.datetime = "20190807131500"
self.assertEqual(
datetime.datetime(2019, 8, 7, 13, 15),
contourusb.ContourHidDevice.get_datetime(self),
datetime.datetime(2019, 8, 7, 13, 15, 0),
contourusb.ContourHidDevice.get_datetime(self.mock_dev),
)

def test_RECORD_FORMAT_match(self):
Expand Down Expand Up @@ -92,7 +96,8 @@ def test_parse_header_record(self):
self.assertEqual(self.mock_dev.total, "1714")
self.assertEqual(self.mock_dev.spec_ver, "1")

self.assertEqual(self.mock_dev.datetime, "201909221304")
# datetime is padded to 14 chars (YYYYMMDDHHMMSS)
self.assertEqual(self.mock_dev.datetime, "20190922130400")

# TO-DO checksum and checkframe unit tests

Expand Down
Loading