summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDiego Elio Pettenò <flameeyes@flameeyes.com>2020-10-04 16:12:44 +0200
committermergify[bot] <37929162+mergify[bot]@users.noreply.github.com>2020-10-04 21:47:57 +0200
commit81d4e308a36cd06f5e7bac30d2af10145c8f193a (patch)
treeacf5314486de90f79ec4b77b423f282e7a9afaa4
parenttd4277: update dependency information for pyserial. (diff)
downloadglucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar.gz
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar.bz2
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar.lz
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar.xz
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.tar.zst
glucometerutils-81d4e308a36cd06f5e7bac30d2af10145c8f193a.zip
-rw-r--r--LICENSES/Apache-2.0.txt208
-rw-r--r--README.md11
-rw-r--r--glucometerutils/support/freestyle.py275
-rw-r--r--glucometerutils/support/tests/test_freestyle.py24
-rw-r--r--reversing_tools/__init__.py3
-rw-r--r--reversing_tools/abbott/__init__.py3
-rw-r--r--reversing_tools/abbott/encrypted_setup_extractor.py179
-rwxr-xr-xreversing_tools/abbott/extract_freestyle.py245
-rwxr-xr-xreversing_tools/abbott/freestyle_hid_console.py69
-rw-r--r--reversing_tools/abbott/known-commands.txt48
-rw-r--r--reversing_tools/abbott/known-commands.txt.license3
-rw-r--r--setup.py8
12 files changed, 25 insertions, 1051 deletions
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
deleted file mode 100644
index 527a83a..0000000
--- a/LICENSES/Apache-2.0.txt
+++ /dev/null
@@ -1,208 +0,0 @@
-Apache License
-
-Version 2.0, January 2004
-
-http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
-AND DISTRIBUTION
-
- 1. Definitions.
-
-
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution
-as defined by Sections 1 through 9 of this document.
-
-
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright
-owner that is granting the License.
-
-
-
-"Legal Entity" shall mean the union of the acting entity and all other entities
-that control, are controlled by, or are under common control with that entity.
-For the purposes of this definition, "control" means (i) the power, direct
-or indirect, to cause the direction or management of such entity, whether
-by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
-of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
-granted by this License.
-
-
-
-"Source" form shall mean the preferred form for making modifications, including
-but not limited to software source code, documentation source, and configuration
-files.
-
-
-
-"Object" form shall mean any form resulting from mechanical transformation
-or translation of a Source form, including but not limited to compiled object
-code, generated documentation, and conversions to other media types.
-
-
-
-"Work" shall mean the work of authorship, whether in Source or Object form,
-made available under the License, as indicated by a copyright notice that
-is included in or attached to the work (an example is provided in the Appendix
-below).
-
-
-
-"Derivative Works" shall mean any work, whether in Source or Object form,
-that is based on (or derived from) the Work and for which the editorial revisions,
-annotations, elaborations, or other modifications represent, as a whole, an
-original work of authorship. For the purposes of this License, Derivative
-Works shall not include works that remain separable from, or merely link (or
-bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-
-
-"Contribution" shall mean any work of authorship, including the original version
-of the Work and any modifications or additions to that Work or Derivative
-Works thereof, that is intentionally submitted to Licensor for inclusion in
-the Work by the copyright owner or by an individual or Legal Entity authorized
-to submit on behalf of the copyright owner. For the purposes of this definition,
-"submitted" means any form of electronic, verbal, or written communication
-sent to the Licensor or its representatives, including but not limited to
-communication on electronic mailing lists, source code control systems, and
-issue tracking systems that are managed by, or on behalf of, the Licensor
-for the purpose of discussing and improving the Work, but excluding communication
-that is conspicuously marked or otherwise designated in writing by the copyright
-owner as "Not a Contribution."
-
-
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
-of whom a Contribution has been received by Licensor and subsequently incorporated
-within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this
-License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
-no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
-Derivative Works of, publicly display, publicly perform, sublicense, and distribute
-the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License,
-each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
-no-charge, royalty-free, irrevocable (except as stated in this section) patent
-license to make, have made, use, offer to sell, sell, import, and otherwise
-transfer the Work, where such license applies only to those patent claims
-licensable by such Contributor that are necessarily infringed by their Contribution(s)
-alone or by combination of their Contribution(s) with the Work to which such
-Contribution(s) was submitted. If You institute patent litigation against
-any entity (including a cross-claim or counterclaim in a lawsuit) alleging
-that the Work or a Contribution incorporated within the Work constitutes direct
-or contributory patent infringement, then any patent licenses granted to You
-under this License for that Work shall terminate as of the date such litigation
-is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or
-Derivative Works thereof in any medium, with or without modifications, and
-in Source or Object form, provided that You meet the following conditions:
-
-(a) You must give any other recipients of the Work or Derivative Works a copy
-of this License; and
-
-(b) You must cause any modified files to carry prominent notices stating that
-You changed the files; and
-
-(c) You must retain, in the Source form of any Derivative Works that You distribute,
-all copyright, patent, trademark, and attribution notices from the Source
-form of the Work, excluding those notices that do not pertain to any part
-of the Derivative Works; and
-
-(d) If the Work includes a "NOTICE" text file as part of its distribution,
-then any Derivative Works that You distribute must include a readable copy
-of the attribution notices contained within such NOTICE file, excluding those
-notices that do not pertain to any part of the Derivative Works, in at least
-one of the following places: within a NOTICE text file distributed as part
-of the Derivative Works; within the Source form or documentation, if provided
-along with the Derivative Works; or, within a display generated by the Derivative
-Works, if and wherever such third-party notices normally appear. The contents
-of the NOTICE file are for informational purposes only and do not modify the
-License. You may add Your own attribution notices within Derivative Works
-that You distribute, alongside or as an addendum to the NOTICE text from the
-Work, provided that such additional attribution notices cannot be construed
-as modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide
-additional or different license terms and conditions for use, reproduction,
-or distribution of Your modifications, or for any such Derivative Works as
-a whole, provided Your use, reproduction, and distribution of the Work otherwise
-complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any
-Contribution intentionally submitted for inclusion in the Work by You to the
-Licensor shall be under the terms and conditions of this License, without
-any additional terms or conditions. Notwithstanding the above, nothing herein
-shall supersede or modify the terms of any separate license agreement you
-may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names,
-trademarks, service marks, or product names of the Licensor, except as required
-for reasonable and customary use in describing the origin of the Work and
-reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to
-in writing, Licensor provides the Work (and each Contributor provides its
-Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied, including, without limitation, any warranties
-or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
-A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
-of using or redistributing the Work and assume any risks associated with Your
-exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether
-in tort (including negligence), contract, or otherwise, unless required by
-applicable law (such as deliberate and grossly negligent acts) or agreed to
-in writing, shall any Contributor be liable to You for damages, including
-any direct, indirect, special, incidental, or consequential damages of any
-character arising as a result of this License or out of the use or inability
-to use the Work (including but not limited to damages for loss of goodwill,
-work stoppage, computer failure or malfunction, or any and all other commercial
-damages or losses), even if such Contributor has been advised of the possibility
-of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work
-or Derivative Works thereof, You may choose to offer, and charge a fee for,
-acceptance of support, warranty, indemnity, or other liability obligations
-and/or rights consistent with this License. However, in accepting such obligations,
-You may act only on Your own behalf and on Your sole responsibility, not on
-behalf of any other Contributor, and only if You agree to indemnify, defend,
-and hold each Contributor harmless for any liability incurred by, or claims
-asserted against, such Contributor by reason of your accepting any such warranty
-or additional liability. END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
-To apply the Apache License to your work, attach the following boilerplate
-notice, with the fields enclosed by brackets "[]" replaced with your own identifying
-information. (Don't include the brackets!) The text should be enclosed in
-the appropriate comment syntax for the file format. We also recommend that
-a file or class name and description of purpose be included on the same "printed
-page" as the copyright notice for easier identification within third-party
-archives.
-
-Copyright [yyyy] [name of copyright owner]
-
-Licensed under the Apache License, Version 2.0 (the "License");
-
-you may not use this file except in compliance with the License.
-
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-
-distributed under the License is distributed on an "AS IS" BASIS,
-
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
-See the License for the specific language governing permissions and
-
-limitations under the License.
diff --git a/README.md b/README.md
index 560e454..92e4ad0 100644
--- a/README.md
+++ b/README.md
@@ -53,12 +53,12 @@ supported.
| LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] |
| LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] |
| LifeScan | OneTouch Select Plus Flex¹ | `otverio2015` | [construct] [python-scsi] |
-| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ |
-| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ |
+| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [freestyle_hid] [hidapi]‡ |
+| Abbott | FreeStyle Libre | `fslibre` | [freestyle_hid] [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]‡ |
+| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ |
+| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ |
+| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [freestyle_hid] [hidapi]‡ |
| Roche | Accu-Chek Mobile | `accuchek_reports` | |
| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] |
| TaiDoc | TD-4277 | `td4277` | [construct] [pyserial]² [hidapi] |
@@ -86,6 +86,7 @@ please provide a reference, possibly by writing a specification and contribute
it to https://protocols.glucometers.tech/ .
[construct]: https://construct.readthedocs.io/en/latest/
+[freestyle-hid]: https://pypi.org/project/freestyle-hid/
[pyserial]: https://pythonhosted.org/pyserial/
[python-scsi]: https://pypi.org/project/PYSCSI/
[hidapi]: https://pypi.python.org/pypi/hidapi
diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py
index 13e48eb..28aec6a 100644
--- a/glucometerutils/support/freestyle.py
+++ b/glucometerutils/support/freestyle.py
@@ -9,110 +9,13 @@ https://protocols.glucometers.tech/abbott/shared-hid-protocol
"""
-import csv
import datetime
-import logging
-import re
-from typing import AnyStr, Callable, Iterator, List, Optional, Tuple
+import pathlib
+from typing import Optional
-import construct
+import freestyle_hid
from glucometerutils import driver, exceptions
-from glucometerutils.support import hiddevice
-
-_INIT_COMMAND = 0x01
-_INIT_RESPONSE = 0x71
-
-_KEEPALIVE_RESPONSE = 0x22
-_UNKNOWN_MESSAGE_RESPONSE = 0x30
-
-_ENCRYPTION_SETUP_COMMAND = 0x14
-_ENCRYPTION_SETUP_RESPONSE = 0x33
-
-_ALWAYS_UNENCRYPTED_MESSAGES = (
- _INIT_COMMAND,
- 0x04,
- 0x05,
- 0x06,
- 0x0C,
- 0x0D,
- _ENCRYPTION_SETUP_COMMAND,
- 0x15,
- _ENCRYPTION_SETUP_RESPONSE,
- 0x34,
- 0x35,
- _INIT_RESPONSE,
- _KEEPALIVE_RESPONSE,
-)
-
-
-def _create_matcher(
- message_type: int, content: Optional[bytes]
-) -> Callable[[Tuple[int, bytes]], bool]:
- def _matcher(message: Tuple[int, bytes]) -> bool:
- return message[0] == message_type and (content is None or content == message[1])
-
- return _matcher
-
-
-_is_init_reply = _create_matcher(_INIT_RESPONSE, b"\x01")
-_is_keepalive_response = _create_matcher(_KEEPALIVE_RESPONSE, None)
-_is_unknown_message_error = _create_matcher(_UNKNOWN_MESSAGE_RESPONSE, b"\x85")
-_is_encryption_missing_error = _create_matcher(_ENCRYPTION_SETUP_RESPONSE, b"\x15")
-_is_encryption_setup_error = _create_matcher(_ENCRYPTION_SETUP_RESPONSE, b"\x14")
-
-_FREESTYLE_MESSAGE = construct.Struct(
- 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.
- construct.Prefixed(construct.Byte, construct.GreedyBytes),
- ),
-)
-
-_FREESTYLE_ENCRYPTED_MESSAGE = construct.Struct(
- 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.
- construct.GreedyBytes,
- ),
-)
-
-_TEXT_COMPLETION_RE = re.compile(b"CMD (?:OK|Fail!)")
-_TEXT_REPLY_FORMAT = re.compile(
- b"^(?P<message>.*)CKSM:(?P<checksum>[0-9A-F]{8})\r\n"
- b"CMD (?P<status>OK|Fail!)\r\n$",
- re.DOTALL,
-)
-
-_MULTIRECORDS_FORMAT = re.compile(
- "^(?P<message>.+\r\n)(?P<count>[0-9]+),(?P<checksum>[0-9A-F]{8})\r\n$", re.DOTALL
-)
-
-
-def _verify_checksum(message: AnyStr, expected_checksum_hex: AnyStr) -> None:
- """Calculate the simple checksum of the message and compare with expected.
-
- Args:
- message: (str) message to calculate the checksum of.
- expected_checksum_hex: hexadecimal string representing the checksum
- expected to match the message.
-
- Raises:
- InvalidChecksum: if the message checksum calculated does not match the one
- received.
- """
- expected_checksum = int(expected_checksum_hex, 16)
- if isinstance(message, bytes):
- all_bytes = (c for c in message)
- else:
- all_bytes = (ord(c) for c in message)
-
- calculated_checksum = sum(all_bytes)
-
- if expected_checksum != calculated_checksum:
- raise exceptions.InvalidChecksum(expected_checksum, calculated_checksum)
def convert_ketone_unit(raw_value: float) -> float:
@@ -129,161 +32,6 @@ def convert_ketone_unit(raw_value: float) -> float:
ABBOTT_VENDOR_ID = 0x1A61
-class FreeStyleHidSession:
- def __init__(
- self,
- product_id: int,
- device_path: Optional[str],
- text_message_type: int,
- text_reply_message_type: int,
- ) -> None:
-
- self._hid_session = hiddevice.HidSession(
- (ABBOTT_VENDOR_ID, product_id), device_path
- )
- self._text_message_type = text_message_type
- self._text_reply_message_type = text_reply_message_type
-
- def connect(self):
- """Open connection to the device, starting the knocking sequence."""
- self.send_command(_INIT_COMMAND, b"")
- response = self.read_response()
- if not _is_init_reply(response):
- raise exceptions.ConnectionFailed(
- f"Connection error: unexpected message %{response[0]:02x}:{response[1].hex()}"
- )
-
- def send_command(self, message_type: int, command: bytes, encrypted: bool = False):
- """Send a raw command to the device.
-
- Args:
- message_type: The first byte sent with the report to the device.
- command: The command to send out the device.
- """
- if encrypted:
- assert message_type not in _ALWAYS_UNENCRYPTED_MESSAGES
- meta_construct = _FREESTYLE_ENCRYPTED_MESSAGE
- else:
- meta_construct = _FREESTYLE_MESSAGE
-
- usb_packet = meta_construct.build(
- {"message_type": message_type, "command": command}
- )
-
- logging.debug("Sending packet: %r", usb_packet)
- self._hid_session.write(usb_packet)
-
- def read_response(self, encrypted: bool = False) -> Tuple[int, bytes]:
- """Read the response from the device and extracts it."""
- usb_packet = self._hid_session.read()
-
- logging.debug("Read packet: %r", usb_packet)
-
- assert usb_packet
- message_type = usb_packet[0]
-
- if not encrypted or message_type in _ALWAYS_UNENCRYPTED_MESSAGES:
- message_length = usb_packet[1]
- message_end_idx = 2 + message_length
- message_content = usb_packet[2:message_end_idx]
- else:
- message_content = usb_packet[1:]
-
- # hidapi module returns a list of bytes rather than a bytes object.
- message = (message_type, bytes(message_content))
-
- # There appears to be a stray number of 22 01 xx messages being returned
- # by some devices after commands are sent. These do not appear to have
- # meaning, so ignore them and proceed to the next. These are always sent
- # unencrypted, so we need to inspect them before we decide what the
- # message content is.
- if _is_keepalive_response(message):
- return self.read_response(encrypted=encrypted)
-
- if _is_unknown_message_error(message):
- raise exceptions.CommandError("Invalid command")
-
- if _is_encryption_missing_error(message):
- raise exceptions.CommandError("Device encryption not initialized.")
-
- if _is_encryption_setup_error(message):
- raise exceptions.CommandError("Device encryption initialization failed.")
-
- return message
-
- def send_text_command(self, command: bytes) -> str:
- """Send a command to the device that expects a text reply."""
- self.send_command(self._text_message_type, command)
-
- # Reply can stretch multiple buffers
- full_content = b""
- while True:
- message_type, content = self.read_response()
-
- logging.debug(
- "Received message: type %02x content %s", message_type, content.hex()
- )
-
- if message_type != self._text_reply_message_type:
- raise exceptions.InvalidResponse(
- f"Message type {message_type:02x}: content does not match expectations: {content!r}"
- )
-
- full_content += content
-
- if _TEXT_COMPLETION_RE.search(full_content):
- break
-
- match = _TEXT_REPLY_FORMAT.search(full_content)
- if not match:
- raise exceptions.InvalidResponse(repr(full_content))
-
- message = match.group("message")
- _verify_checksum(message, match.group("checksum"))
-
- if match.group("status") != b"OK":
- raise exceptions.InvalidResponse(repr(message) or "Command failed")
-
- # If there is anything in the response that is not ASCII-safe, this is
- # probably in the patient name. The Windows utility does not seem to
- # validate those, so just replace anything non-ASCII with the correct
- # unknown codepoint.
- return message.decode("ascii", "replace")
-
- def query_multirecord(self, command: bytes) -> Iterator[List[str]]:
- """Queries for, and returns, "multirecords" results.
-
- Multirecords are used for querying events, readings, history and similar
- other data out of a FreeStyle device. These are comma-separated values,
- variable-length.
-
- The validation includes the general HID framing parsing, as well as
- validation of the record count, and of the embedded records checksum.
-
- Args:
- command: The text command to send to the device for the query.
-
- Returns:
- A CSV reader object that returns a record for each line in the
- reply buffer.
- """
- message = self.send_text_command(command)
- logging.debug("Received multirecord message:\n%s", message)
- if message == "Log Empty\r\n":
- return iter(())
-
- match = _MULTIRECORDS_FORMAT.search(message)
- if not match:
- raise exceptions.InvalidResponse(message)
-
- records_str = match.group("message")
- _verify_checksum(records_str, match.group("checksum"))
-
- logging.debug("Received multi-record string: %s", records_str)
-
- return csv.reader(records_str.split("\r\n"))
-
-
class FreeStyleHidDevice(driver.GlucometerDevice):
"""Base class implementing the FreeStyle HID common protocol.
@@ -304,13 +52,22 @@ class FreeStyleHidDevice(driver.GlucometerDevice):
text_reply_cmd: int = 0x60,
) -> None:
super().__init__(device_path)
- self._session = FreeStyleHidSession(
- product_id, device_path, text_cmd, text_reply_cmd
- )
+ try:
+ self._session = freestyle_hid.Session(
+ product_id,
+ pathlib.Path(device_path) if device_path else None,
+ text_cmd,
+ text_reply_cmd,
+ )
+ except Exception as e:
+ raise exceptions.ConnectionFailed(str(e))
def connect(self) -> None:
"""Open connection to the device, starting the knocking sequence."""
- self._session.connect()
+ try:
+ self._session.connect()
+ except Exception as e:
+ raise exceptions.ConnectionFailed(str(e))
def disconnect(self) -> None:
"""Disconnect the device, nothing to be done."""
diff --git a/glucometerutils/support/tests/test_freestyle.py b/glucometerutils/support/tests/test_freestyle.py
deleted file mode 100644
index fd3f403..0000000
--- a/glucometerutils/support/tests/test_freestyle.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# SPDX-FileCopyrightText: © 2019 The glucometerutils Authors
-# SPDX-License-Identifier: MIT
-"""Tests for the common FreeStyle functions.."""
-
-# pylint: disable=protected-access,missing-docstring
-
-from absl.testing import absltest
-
-from glucometerutils.support import freestyle
-
-
-class TestFreeStyle(absltest.TestCase):
- def test_outgoing_command(self):
- """Test the generation of a new outgoing message."""
-
- self.assertEqual(
- b"\0\x17\7command\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
- b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
- freestyle._FREESTYLE_MESSAGE.build(
- {"message_type": 23, "command": b"command"}
- ),
- )
diff --git a/reversing_tools/__init__.py b/reversing_tools/__init__.py
deleted file mode 100644
index 4b386c3..0000000
--- a/reversing_tools/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# SPDX-FileCopyrightText: 2013 The glucometerutils Authors
-#
-# SPDX-License-Identifier: Unlicense
diff --git a/reversing_tools/abbott/__init__.py b/reversing_tools/abbott/__init__.py
deleted file mode 100644
index 4b386c3..0000000
--- a/reversing_tools/abbott/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# SPDX-FileCopyrightText: 2013 The glucometerutils Authors
-#
-# SPDX-License-Identifier: Unlicense
diff --git a/reversing_tools/abbott/encrypted_setup_extractor.py b/reversing_tools/abbott/encrypted_setup_extractor.py
deleted file mode 100644
index cc57f0f..0000000
--- a/reversing_tools/abbott/encrypted_setup_extractor.py
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/usr/bin/env python3
-#
-# SPDX-FileCopyrightText: © 2019 The usbmon-tools Authors
-# SPDX-FileCopyrightText: © 2020 The glucometerutils Authors
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import argparse
-import logging
-import sys
-
-import construct
-import usbmon
-import usbmon.pcapng
-
-_SERIAL_NUMBER_RESPONSE_TYPE = 0x06
-_ENCRYPTION_SETUP_REQ_TYPE = 0x14
-_ENCRYPTION_SETUP_RESP_TYPE = 0x33
-
-
-_START_AUTHORIZE_CMD = 0x11
-_CHALLENGE_CMD = 0x16
-_CHALLENGE_RESPONSE_CMD = 0x17
-
-
-_ABBOTT_VENDOR_ID = 0x1A61
-_LIBRE2_PRODUCT_ID = 0x3950
-
-_SERIAL_NO = construct.Struct(
- message_type=construct.Const(_SERIAL_NUMBER_RESPONSE_TYPE, construct.Byte),
- length=construct.Const(14, construct.Byte),
- serial_number=construct.PaddedString(13, "ascii"),
- termination=construct.Const(0, construct.Byte),
-)
-
-_CHALLENGE = construct.Struct(
- message_type=construct.Const(_ENCRYPTION_SETUP_RESP_TYPE, construct.Byte),
- length=construct.Const(16, construct.Byte),
- subcmd=construct.Const(_CHALLENGE_CMD, construct.Byte),
- challenge=construct.Bytes(8),
- iv=construct.Bytes(7),
-)
-
-_CHALLENGE_RESPONSE = construct.Struct(
- message_type=construct.Const(_ENCRYPTION_SETUP_REQ_TYPE, construct.Byte),
- length=construct.Const(26, construct.Byte),
- subcmd=construct.Const(_CHALLENGE_RESPONSE_CMD, construct.Byte),
- challenge_response_encrypted=construct.Bytes(16),
- const=construct.Const(1, construct.Byte),
- mac=construct.Bytes(8),
-)
-
-
-def main():
- if sys.version_info < (3, 7):
- raise Exception("Unsupported Python version, please use at least Python 3.7.")
-
- parser = argparse.ArgumentParser()
-
- parser.add_argument(
- "--device_address",
- action="store",
- type=str,
- help=(
- "Device address (busnum.devnum) of the device to extract capture"
- " of. If none provided, device descriptors will be relied on."
- ),
- )
-
- parser.add_argument(
- "--vlog",
- action="store",
- required=False,
- type=int,
- help=(
- "Python logging level. See the levels at"
- " https://docs.python.org/3/library/logging.html#logging-levels"
- ),
- )
-
- parser.add_argument(
- "pcap_files",
- action="store",
- type=argparse.FileType(mode="rb"),
- help="Path to the pcapng file with the USB capture.",
- nargs="+",
- )
-
- args = parser.parse_args()
-
- logging.basicConfig(level=args.vlog)
-
- for pcap_file in args.pcap_files:
- session = usbmon.pcapng.parse_stream(pcap_file, retag_urbs=False)
-
- if not args.device_address:
- for descriptor in session.device_descriptors.values():
- if (
- descriptor.vendor_id == _ABBOTT_VENDOR_ID
- and descriptor.product_id == _LIBRE2_PRODUCT_ID
- ):
- if (
- args.device_address
- and args.device_address != descriptor.address
- ):
- raise Exception(
- "Multiple Libre2 devices present in capture, please"
- " provide a --device_address flag."
- )
- device_address = descriptor.address
- else:
- device_address = descriptor.address
-
- descriptor = session.device_descriptors.get(device_address, None)
- if descriptor:
- assert descriptor.vendor_id == _ABBOTT_VENDOR_ID
- assert descriptor.product_id == _LIBRE2_PRODUCT_ID
-
- serial_number = "UNKNOWN"
- challenge = "UNKNOWN"
- iv = "UNKNOWN"
- encrypted_challenge = "UNKNOWN"
- mac = "UNKNOWN"
-
- for first, second in session.in_pairs():
- # Ignore stray callbacks/errors.
- if not first.type == usbmon.constants.PacketType.SUBMISSION:
- continue
-
- if not first.address.startswith(f"{device_address}."):
- # No need to check second, they will be linked.
- continue
-
- if first.xfer_type == usbmon.constants.XferType.INTERRUPT:
- pass
- elif (
- first.xfer_type == usbmon.constants.XferType.CONTROL
- and not first.setup_packet
- or first.setup_packet.type == usbmon.setup.Type.CLASS
- ):
- pass
- else:
- continue
-
- if first.direction == usbmon.constants.Direction.OUT:
- packet = first
- else:
- packet = second
-
- if not packet.payload:
- continue
-
- assert len(packet.payload) >= 2
-
- message_type = packet.payload[0]
-
- if message_type == _SERIAL_NUMBER_RESPONSE_TYPE:
- obj = _SERIAL_NO.parse(packet.payload)
- serial_number = obj.serial_number
- elif (
- message_type == _ENCRYPTION_SETUP_RESP_TYPE
- and packet.payload[2] == _CHALLENGE_CMD
- ):
- obj = _CHALLENGE.parse(packet.payload)
- challenge = obj.challenge.hex()
- iv = obj.iv.hex()
- elif (
- message_type == _ENCRYPTION_SETUP_REQ_TYPE
- and packet.payload[2] == _CHALLENGE_RESPONSE_CMD
- ):
- obj = _CHALLENGE_RESPONSE.parse(packet.payload)
- encrypted_challenge = obj.challenge_response_encrypted.hex()
- mac = obj.mac.hex()
-
- print(f"{serial_number},{challenge},{iv},{encrypted_challenge},{mac}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/reversing_tools/abbott/extract_freestyle.py b/reversing_tools/abbott/extract_freestyle.py
deleted file mode 100755
index 0c0888a..0000000
--- a/reversing_tools/abbott/extract_freestyle.py
+++ /dev/null
@@ -1,245 +0,0 @@
-#!/usr/bin/env python3
-#
-# SPDX-FileCopyrightText: © 2019 The usbmon-tools Authors
-# SPDX-FileCopyrightText: © 2020 The glucometerutils Authors
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import argparse
-import logging
-import sys
-import textwrap
-
-import construct
-import usbmon
-import usbmon.chatter
-import usbmon.pcapng
-
-_KEEPALIVE_TYPE = 0x22
-
-_UNENCRYPTED_TYPES = (
- 0x01,
- 0x04,
- 0x05,
- 0x06,
- 0x0C,
- 0x0D,
- 0x14,
- 0x15,
- 0x33,
- 0x34,
- 0x35,
- 0x71,
- _KEEPALIVE_TYPE,
-)
-
-_ENCRYPTION_SETUP_TYPES = (0x14, 0x33)
-
-_START_AUTHORIZE_CMD = 0x11
-_CHALLENGE_CMD = 0x16
-_CHALLENGE_RESPONSE_CMD = 0x17
-_CHALLENGE_ACCEPTED_CMD = 0x18
-
-_ABBOTT_VENDOR_ID = 0x1A61
-_LIBRE2_PRODUCT_ID = 0x3950
-
-_ENCRYPTED_MESSAGE = construct.Struct(
- message_type=construct.Byte,
- encrypted_message=construct.Bytes(64 - 1 - 4 - 4),
- sequence_number=construct.Int32ul,
- mac=construct.Int32ul,
-)
-
-
-def main():
- if sys.version_info < (3, 7):
- raise Exception("Unsupported Python version, please use at least Python 3.7.")
-
- parser = argparse.ArgumentParser()
-
- parser.add_argument(
- "--device_address",
- action="store",
- type=str,
- help=(
- "Device address (busnum.devnum) of the device to extract capture"
- " of. If none provided, device descriptors will be relied on."
- ),
- )
-
- parser.add_argument(
- "--encrypted_protocol",
- action="store_true",
- help=(
- "Whether to expect encrypted protocol in the capture."
- " Ignored if the device descriptors are present in the capture."
- ),
- )
-
- parser.add_argument(
- "--verbose-encryption-setup",
- action="store_true",
- help=(
- "Whether to parse encryption setup commands and printing their component"
- " together with the raw messsage."
- ),
- )
-
- parser.add_argument(
- "--vlog",
- action="store",
- required=False,
- type=int,
- help=(
- "Python logging level. See the levels at"
- " https://docs.python.org/3/library/logging.html#logging-levels"
- ),
- )
-
- parser.add_argument(
- "--print_keepalive",
- action="store_true",
- help=(
- "Whether to print the keepalive messages sent by the device. "
- "Keepalive messages are usually safely ignored."
- ),
- )
-
- parser.add_argument(
- "pcap_file",
- action="store",
- type=argparse.FileType(mode="rb"),
- help="Path to the pcapng file with the USB capture.",
- )
-
- args = parser.parse_args()
-
- logging.basicConfig(level=args.vlog)
-
- session = usbmon.pcapng.parse_stream(args.pcap_file, retag_urbs=False)
-
- if not args.device_address:
- for descriptor in session.device_descriptors.values():
- if descriptor.vendor_id == _ABBOTT_VENDOR_ID:
- if args.device_address and args.device_address != descriptor.address:
- raise Exception(
- "Multiple Abbott device present in capture, please"
- " provide a --device_address flag."
- )
- args.device_address = descriptor.address
-
- descriptor = session.device_descriptors.get(args.device_address, None)
- if not descriptor:
- logging.warning(
- "Unable to find device %s in the capture's descriptors."
- " Assuming non-encrypted protocol.",
- args.device_address,
- )
- else:
- assert descriptor.vendor_id == _ABBOTT_VENDOR_ID
-
- if descriptor and descriptor.product_id == _LIBRE2_PRODUCT_ID:
- args.encrypted_protocol = True
-
- for first, second in session.in_pairs():
- # Ignore stray callbacks/errors.
- if not first.type == usbmon.constants.PacketType.SUBMISSION:
- continue
-
- if not first.address.startswith(f"{args.device_address}."):
- # No need to check second, they will be linked.
- continue
-
- if first.xfer_type == usbmon.constants.XferType.INTERRUPT:
- pass
- elif (
- first.xfer_type == usbmon.constants.XferType.CONTROL
- and not first.setup_packet
- or first.setup_packet.type == usbmon.setup.Type.CLASS
- ):
- pass
- else:
- continue
-
- if first.direction == usbmon.constants.Direction.OUT:
- packet = first
- else:
- packet = second
-
- if not packet.payload:
- continue
-
- assert len(packet.payload) >= 2
-
- message_type = packet.payload[0]
-
- if message_type == _KEEPALIVE_TYPE and not args.print_keepalive:
- continue
-
- message_metadata = []
-
- if args.encrypted_protocol and message_type not in _UNENCRYPTED_TYPES:
- # With encrypted communication, the length of the message is also encrypted,
- # and all the packets use the full 64 bytes. So instead, we extract what
- # metadata we can.
- parsed = _ENCRYPTED_MESSAGE.parse(packet.payload)
- message_metadata.extend(
- [f"SEQUENCE_NUMBER={parsed.sequence_number}", f"MAC={parsed.mac:04x}"]
- )
-
- message_type = f"x{message_type:02x}"
- message = parsed.encrypted_message
- elif args.verbose_encryption_setup and message_type in _ENCRYPTION_SETUP_TYPES:
- message_length = packet.payload[1]
- message_end_idx = 2 + message_length
- message = packet.payload[2:message_end_idx]
-
- if message[0] == _START_AUTHORIZE_CMD:
- message_metadata.append("START_AUTHORIZE")
- elif message[0] == _CHALLENGE_CMD:
- message_metadata.append("CHALLENGE")
- challenge = message[1:9]
- iv = message[9:16]
- message_metadata.append(f"CHALLENGE={challenge.hex()}")
- message_metadata.append(f"IV={iv.hex()}")
- elif message[0] == _CHALLENGE_RESPONSE_CMD:
- message_metadata.append("CHALLENGE_RESPONSE")
- encrypted_challenge = message[1:17]
- challenge_mac = message[18:26]
- message_metadata.append(
- f"ENCRYPTED_CHALLENGE={encrypted_challenge.hex()}"
- )
- message_metadata.append(f"MAC={challenge_mac.hex()}")
- elif message[0] == _CHALLENGE_ACCEPTED_CMD:
- message_metadata.append("CHALLENGE_ACCEPTED")
-
- message_metadata.append(f"RAW_LENGTH={message_length}")
- message_type = f" {message_type:02x}"
- else:
- message_length = packet.payload[1]
- message_metadata.append(f"LENGTH={message_length}")
- message_end_idx = 2 + message_length
- message_type = f" {message_type:02x}"
- message = packet.payload[2:message_end_idx]
-
- if message_metadata:
- metadata_string = "\n".join(
- textwrap.wrap(
- " ".join(message_metadata), width=80, break_long_words=False
- )
- )
- print(metadata_string)
-
- print(
- usbmon.chatter.dump_bytes(
- packet.direction,
- message,
- prefix=f"[{message_type}]",
- print_empty=True,
- ),
- "\n",
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/reversing_tools/abbott/freestyle_hid_console.py b/reversing_tools/abbott/freestyle_hid_console.py
deleted file mode 100755
index 18df89c..0000000
--- a/reversing_tools/abbott/freestyle_hid_console.py
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-FileCopyrightText: © 2019 The glucometerutils Authors
-# SPDX-License-Identifier: MIT
-"""CLI tool to send messages through FreeStyle HID protocol."""
-
-import argparse
-import logging
-import sys
-
-from glucometerutils import exceptions
-from glucometerutils.support import freestyle
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "--text_cmd_type",
- action="store",
- type=int,
- default=0x60,
- help="Message type for text commands sent to the device.",
- )
- parser.add_argument(
- "--text_reply_type",
- action="store",
- type=int,
- default=0x60,
- help="Message type for text replies received from the device.",
- )
- parser.add_argument(
- "device", action="store", help="Path to the HID device to open."
- )
-
- parser.add_argument(
- "--vlog",
- action="store",
- required=False,
- type=int,
- help=(
- "Python logging level. See the levels at "
- "https://docs.python.org/3/library/logging.html#logging-levels"
- ),
- )
-
- args = parser.parse_args()
-
- logging.basicConfig(level=args.vlog)
-
- session = freestyle.FreeStyleHidSession(
- None, args.device, args.text_cmd_type, args.text_reply_type
- )
-
- session.connect()
-
- while True:
- if sys.stdin.isatty():
- command = input(">>> ")
- else:
- command = input()
- print(f">>> {command}")
-
- try:
- print(session.send_text_command(bytes(command, "ascii")))
- except exceptions.InvalidResponse as error:
- print(f"! {error}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/reversing_tools/abbott/known-commands.txt b/reversing_tools/abbott/known-commands.txt
deleted file mode 100644
index be3f9f0..0000000
--- a/reversing_tools/abbott/known-commands.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-$getrmndrst,0
-$getrmndr,0
-$rmdstrorder?
-$actthm?
-$wktrend?
-$gunits?
-$clktyp?
-$alllang?
-$lang?
-$inslock?
-$actinscal?
-$iobstatus?
-$foodunits?
-$svgsdef?
-$corsetup?
-$insdose?
-$inslog?
-$inscalsetup?
-$carbratio?
-$svgsratio?
-$mlcalget,3
-$cttype?
-$bgdrop?
-$bgtrgt?
-$bgtgrng?
-$ntsound?
-$btsound?
-$custthm?
-$taglang?
-$tagsenbl?
-$tagorder?
-$result?
-$gettags,2,2
-$frststrt?
-$marketlev?
-$brandname?
-$uom?
-$temp?
-$cksm?
-$vrom?
-$sn?
-$serlnum?
-$history?
-$ptname?
-$swver?
-$date?
-$time?
-$ptid?
diff --git a/reversing_tools/abbott/known-commands.txt.license b/reversing_tools/abbott/known-commands.txt.license
deleted file mode 100644
index c662d53..0000000
--- a/reversing_tools/abbott/known-commands.txt.license
+++ /dev/null
@@ -1,3 +0,0 @@
-SPDX-FileCopyrightText: 2013 The glucometerutils Authors
-
-SPDX-License-Identifier: MIT
diff --git a/setup.py b/setup.py
index cbe985e..115a0ad 100644
--- a/setup.py
+++ b/setup.py
@@ -12,18 +12,16 @@ extras_require = {
# listed as mandatory for the feature.
"accucheck_reports": [],
"contourusb": ["construct", "hidapi"],
- "fsinsulinx": ["construct", "hidapi"],
- "fslibre": ["construct", "hidapi"],
+ "fsinsulinx": ["freestyle_hid"],
+ "fslibre": ["freestyle_hid"],
"fsoptium": ["pyserial"],
- "fsprecisionneo": ["construct", "hidapi"],
+ "fsprecisionneo": ["freestyle_hid"],
"otultra2": ["pyserial"],
"otultraeasy": ["construct", "pyserial"],
"otverio2015": ["construct", "PYSCSI[sgio]>=2.0.1"],
"otverioiq": ["construct", "pyserial"],
"sdcodefree": ["construct", "pyserial"],
"td4277": ["construct", "pyserial[cp2110]>=3.5b0"],
- # These are not drivers, but rather tools and features.
- "reversing_tools": ["usbmon-tools"],
"dev": [
"absl-py",
"construct>=2.9",