From 5e8127dbf4aa17402671232c8516d30e9716a1a4 Mon Sep 17 00:00:00 2001 From: Kirk Meyer Date: Tue, 14 Jan 2025 07:21:18 -0700 Subject: [PATCH] Add support for Freestyle Libre 3 The Libre 3 reports results in a different way: * The history reports are missing a column of unknown meaning. * The reading reports are missing custom comments and error values. The current workaround makes the Libre 3 results to look like the results for earlier models and is for demonstration and documentation purposes only. A better implementation would abstract the difference in reporting format into the drivers for each device. --- glucometerutils/drivers/fslibre3.py | 48 ++++++++++++++++++++++ glucometerutils/support/freestyle_libre.py | 12 ++++++ 2 files changed, 60 insertions(+) create mode 100644 glucometerutils/drivers/fslibre3.py diff --git a/glucometerutils/drivers/fslibre3.py b/glucometerutils/drivers/fslibre3.py new file mode 100644 index 0000000..eb6c268 --- /dev/null +++ b/glucometerutils/drivers/fslibre3.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +# SPDX-FileCopyrightText: © 2023 The glucometerutils Authors +# SPDX-License-Identifier: MIT +"""Driver for FreeStyle Libre 3 devices. + +Supported features: + The same as the fslibre driver. + +Expected device path: /dev/hidraw9 or similar HID device. Optional when using +HIDAPI. + +This driver is a shim on top of the fslibre driver, forcing encryption to be +enabled for the session and normalizing the returned records. + +Further information on the device protocol can be found at + +https://protocols.glucometers.tech/abbott/freestyle-libre +https://protocols.glucometers.tech/abbott/freestyle-libre-2 + +""" + +from collections.abc import Sequence +from typing import Optional + +from glucometerutils.support import freestyle_libre + + +class Device(freestyle_libre.LibreDevice): + _MODEL_NAME = "FreeStyle Libre 3" + + def __init__(self, device_path: Optional[str]) -> None: + super().__init__(0x3960, device_path, encoding="utf-8", encrypted=True) + + @staticmethod + def _normalize_history_record(record: Sequence[str]) -> Sequence[str]: + """Overridden function as one of the unknown columns is missing.""" + record.insert(10, "0") + return record + + @staticmethod + def _normalize_result_record(record: Sequence[str]) -> Sequence[str]: + """Overridden function as error values and custom comments are missing.""" + record.insert(19, "0") + record.insert(28, 0) + if len(record) > 29: + record = record[:29] + 6*["\"\""] + record[29:] + return record diff --git a/glucometerutils/support/freestyle_libre.py b/glucometerutils/support/freestyle_libre.py index b231449..1abd0a2 100644 --- a/glucometerutils/support/freestyle_libre.py +++ b/glucometerutils/support/freestyle_libre.py @@ -203,6 +203,16 @@ class LibreDevice(freestyle.FreeStyleHidDevice): _MODEL_NAME: str + @staticmethod + def _normalize_history_record(record: Sequence[str]) -> Sequence[str]: + """Normalize a history record to the base column layout.""" + return record + + @staticmethod + def _normalize_result_record(record: Sequence[str]) -> Sequence[str]: + """Normalize a result record to the base column layout.""" + return record + def get_meter_info(self) -> common.MeterInfo: """Return the device information in structured form.""" return common.MeterInfo( @@ -231,6 +241,7 @@ def get_readings(self) -> Generator[common.AnyReading, None, None]: # First of all get the usually longer list of sensor readings, and # convert them to Readings objects. for record in self._session.query_multirecord(b"$history?"): + record = self._normalize_history_record(record) parsed_record = _parse_record(record, _HISTORY_ENTRY_MAP) if not parsed_record or parsed_record["errors"] != 0: @@ -248,6 +259,7 @@ def get_readings(self) -> Generator[common.AnyReading, None, None]: # Then get the results of explicit scans and blood tests (and other # events). for record in self._session.query_multirecord(b"$arresult?"): + record = self._normalize_result_record(record) logging.debug(f"Retrieved arresult: {record!r}") reading = _parse_arresult(record) if reading: