polar-python 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polar_python-0.0.4 → polar_python-0.0.5}/PKG-INFO +18 -7
- {polar_python-0.0.4 → polar_python-0.0.5}/README.md +5 -5
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/constants.py +45 -2
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/device.py +2 -2
- polar_python-0.0.5/polar_python/parsers/__init__.py +37 -0
- polar_python-0.0.5/polar_python/parsers/accelerometer.py +102 -0
- polar_python-0.0.5/polar_python/parsers/bluetooth.py +36 -0
- polar_python-0.0.5/polar_python/parsers/common.py +10 -0
- polar_python-0.0.5/polar_python/parsers/compression.py +120 -0
- polar_python-0.0.5/polar_python/parsers/ecg.py +13 -0
- polar_python-0.0.5/polar_python/parsers/heartrate.py +20 -0
- polar_python-0.0.5/polar_python/parsers/pmd.py +81 -0
- polar_python-0.0.5/polar_python/parsers/ppi.py +70 -0
- polar_python-0.0.5/polar_python/utils.py +1 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/PKG-INFO +18 -7
- polar_python-0.0.5/polar_python.egg-info/SOURCES.txt +22 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/setup.py +1 -1
- polar_python-0.0.4/polar_python/utils.py +0 -165
- polar_python-0.0.4/polar_python.egg-info/SOURCES.txt +0 -13
- {polar_python-0.0.4 → polar_python-0.0.5}/LICENSE +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/__init__.py +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/exceptions.py +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/dependency_links.txt +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/requires.txt +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/top_level.txt +0 -0
- {polar_python-0.0.4 → polar_python-0.0.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: polar-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: polar-python is a Python library for connecting to Polar devices via Bluetooth Low Energy (BLE) using Bleak. It allows querying device capabilities (e.g., ECG, ACC, PPG), exploring configurable options, and streaming parsed data through callback functions.
|
|
5
5
|
Home-page: https://github.com/zHElEARN/polar-python
|
|
6
6
|
Author: Zhe_Learn
|
|
@@ -16,6 +16,17 @@ Requires-Python: >=3.6
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: bleak
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
19
30
|
|
|
20
31
|
# polar-python
|
|
21
32
|
|
|
@@ -96,8 +107,8 @@ async def main():
|
|
|
96
107
|
ecg_settings = MeasurementSettings(
|
|
97
108
|
measurement_type="ECG",
|
|
98
109
|
settings=[
|
|
99
|
-
SettingType(type="SAMPLE_RATE",
|
|
100
|
-
SettingType(type="RESOLUTION",
|
|
110
|
+
SettingType(type="SAMPLE_RATE", values=[130]),
|
|
111
|
+
SettingType(type="RESOLUTION", values=[14]),
|
|
101
112
|
],
|
|
102
113
|
)
|
|
103
114
|
|
|
@@ -105,9 +116,9 @@ async def main():
|
|
|
105
116
|
acc_settings = MeasurementSettings(
|
|
106
117
|
measurement_type="ACC",
|
|
107
118
|
settings=[
|
|
108
|
-
SettingType(type="SAMPLE_RATE",
|
|
109
|
-
SettingType(type="RESOLUTION",
|
|
110
|
-
SettingType(type="RANGE",
|
|
119
|
+
SettingType(type="SAMPLE_RATE", values=[25]),
|
|
120
|
+
SettingType(type="RESOLUTION", values=[16]),
|
|
121
|
+
SettingType(type="RANGE", values=[2]),
|
|
111
122
|
],
|
|
112
123
|
)
|
|
113
124
|
|
|
@@ -77,8 +77,8 @@ async def main():
|
|
|
77
77
|
ecg_settings = MeasurementSettings(
|
|
78
78
|
measurement_type="ECG",
|
|
79
79
|
settings=[
|
|
80
|
-
SettingType(type="SAMPLE_RATE",
|
|
81
|
-
SettingType(type="RESOLUTION",
|
|
80
|
+
SettingType(type="SAMPLE_RATE", values=[130]),
|
|
81
|
+
SettingType(type="RESOLUTION", values=[14]),
|
|
82
82
|
],
|
|
83
83
|
)
|
|
84
84
|
|
|
@@ -86,9 +86,9 @@ async def main():
|
|
|
86
86
|
acc_settings = MeasurementSettings(
|
|
87
87
|
measurement_type="ACC",
|
|
88
88
|
settings=[
|
|
89
|
-
SettingType(type="SAMPLE_RATE",
|
|
90
|
-
SettingType(type="RESOLUTION",
|
|
91
|
-
SettingType(type="RANGE",
|
|
89
|
+
SettingType(type="SAMPLE_RATE", values=[25]),
|
|
90
|
+
SettingType(type="RESOLUTION", values=[16]),
|
|
91
|
+
SettingType(type="RANGE", values=[2]),
|
|
92
92
|
],
|
|
93
93
|
)
|
|
94
94
|
|
|
@@ -31,7 +31,26 @@ PMD_CONTROL_POINT_ERROR_CODES: List[str] = [
|
|
|
31
31
|
PMD_CONTROL_OPERATION_CODE: dict = {"GET": 0x01, "START": 0x02, "STOP": 0x03}
|
|
32
32
|
|
|
33
33
|
# PMD Setting Types
|
|
34
|
-
PMD_SETTING_TYPES: List[str] = [
|
|
34
|
+
PMD_SETTING_TYPES: List[str] = [
|
|
35
|
+
"SAMPLE_RATE",
|
|
36
|
+
"RESOLUTION",
|
|
37
|
+
"RANGE",
|
|
38
|
+
"RANGE_MILLIUNIT",
|
|
39
|
+
"CHANNELS",
|
|
40
|
+
"FACTOR",
|
|
41
|
+
"SECURITY",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# PMD Setting Types to Field Sizes
|
|
45
|
+
PMD_SETTING_TYPES_TO_FIELD_SIZES = {
|
|
46
|
+
"SAMPLE_RATE": 2,
|
|
47
|
+
"RESOLUTION": 2,
|
|
48
|
+
"RANGE": 2,
|
|
49
|
+
"RANGE_MILLIUNIT": 4,
|
|
50
|
+
"CHANNELS": 1,
|
|
51
|
+
"FACTOR": 4,
|
|
52
|
+
"SECURITY": 16,
|
|
53
|
+
}
|
|
35
54
|
|
|
36
55
|
# Timestamp Offset
|
|
37
56
|
TIMESTAMP_OFFSET: int = 946684800000000000
|
|
@@ -42,9 +61,13 @@ class SettingType:
|
|
|
42
61
|
"""Represents a setting type with its array length and possible values."""
|
|
43
62
|
|
|
44
63
|
type: str
|
|
45
|
-
array_length: int
|
|
46
64
|
values: List[int]
|
|
47
65
|
|
|
66
|
+
@property
|
|
67
|
+
def array_length(self) -> int:
|
|
68
|
+
"""Calculate array length from the values list."""
|
|
69
|
+
return len(self.values)
|
|
70
|
+
|
|
48
71
|
|
|
49
72
|
@dataclass
|
|
50
73
|
class MeasurementSettings:
|
|
@@ -78,3 +101,23 @@ class HRData:
|
|
|
78
101
|
|
|
79
102
|
heartrate: int
|
|
80
103
|
rr_intervals: List[float]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class PPISample:
|
|
108
|
+
"""Represents a single PPI sample."""
|
|
109
|
+
|
|
110
|
+
ppi: int
|
|
111
|
+
error_estimate: int
|
|
112
|
+
hr: int
|
|
113
|
+
invalid_ppi: bool
|
|
114
|
+
skin_contact_status: bool
|
|
115
|
+
skin_contact_supported: bool
|
|
116
|
+
timestamp: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class PPIData:
|
|
121
|
+
"""Represents PPI data."""
|
|
122
|
+
|
|
123
|
+
samples: List[PPISample]
|
|
@@ -12,7 +12,7 @@ class PolarDevice:
|
|
|
12
12
|
self,
|
|
13
13
|
address_or_ble_device: Union[str, BLEDevice],
|
|
14
14
|
data_callback: Callable[
|
|
15
|
-
[Union[constants.ECGData, constants.ACCData]], None
|
|
15
|
+
[Union[constants.ECGData, constants.ACCData, constants.PPIData]], None
|
|
16
16
|
] = None,
|
|
17
17
|
heartrate_callback: Callable[[constants.HRData], None] = None,
|
|
18
18
|
) -> None:
|
|
@@ -151,7 +151,7 @@ class PolarDevice:
|
|
|
151
151
|
def set_callback(
|
|
152
152
|
self,
|
|
153
153
|
data_callback: Callable[
|
|
154
|
-
[Union[constants.ECGData, constants.ACCData]], None
|
|
154
|
+
[Union[constants.ECGData, constants.ACCData, constants.PPIData]], None
|
|
155
155
|
] = None,
|
|
156
156
|
heartrate_callback: Callable[[constants.HRData], None] = None,
|
|
157
157
|
) -> None:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .common import byte_to_bitmap
|
|
2
|
+
from .pmd import parse_pmd_data, build_measurement_settings
|
|
3
|
+
from .ecg import parse_ecg_data
|
|
4
|
+
from .accelerometer import parse_acc_data, parse_raw_acc_data, parse_compressed_acc_data
|
|
5
|
+
from .compression import (
|
|
6
|
+
parse_delta_frames_all,
|
|
7
|
+
parse_delta_frame_ref_samples,
|
|
8
|
+
parse_delta_frame,
|
|
9
|
+
)
|
|
10
|
+
from .ppi import parse_ppi_data
|
|
11
|
+
from .heartrate import parse_heartrate_data
|
|
12
|
+
from .bluetooth import parse_bluetooth_data
|
|
13
|
+
|
|
14
|
+
# Export all functions
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Common utilities
|
|
17
|
+
"byte_to_bitmap",
|
|
18
|
+
# PMD parsing
|
|
19
|
+
"parse_pmd_data",
|
|
20
|
+
"build_measurement_settings",
|
|
21
|
+
# ECG parsing
|
|
22
|
+
"parse_ecg_data",
|
|
23
|
+
# Accelerometer parsing
|
|
24
|
+
"parse_acc_data",
|
|
25
|
+
"parse_raw_acc_data",
|
|
26
|
+
"parse_compressed_acc_data",
|
|
27
|
+
# Compression utilities
|
|
28
|
+
"parse_delta_frames_all",
|
|
29
|
+
"parse_delta_frame_ref_samples",
|
|
30
|
+
"parse_delta_frame",
|
|
31
|
+
# PPI parsing
|
|
32
|
+
"parse_ppi_data",
|
|
33
|
+
# Heart rate parsing
|
|
34
|
+
"parse_heartrate_data",
|
|
35
|
+
# Bluetooth parsing
|
|
36
|
+
"parse_bluetooth_data",
|
|
37
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Accelerometer (ACC) data parsing functions."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from .compression import parse_delta_frames_all
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_acc_data(
|
|
8
|
+
data: List[int], timestamp: int, frame_type: int, factor: float = 1.0
|
|
9
|
+
) -> dict:
|
|
10
|
+
"""Parse accelerometer data from a list of integers based on frame type."""
|
|
11
|
+
is_compressed = (frame_type & 0x80) != 0
|
|
12
|
+
actual_frame_type = frame_type & 0x7F
|
|
13
|
+
|
|
14
|
+
# print(f"Frame type: {frame_type}, Is compressed: {is_compressed}, Actual frame type: {actual_frame_type}")
|
|
15
|
+
|
|
16
|
+
if is_compressed:
|
|
17
|
+
return parse_compressed_acc_data(data, timestamp, actual_frame_type, factor)
|
|
18
|
+
else:
|
|
19
|
+
return parse_raw_acc_data(data, timestamp, actual_frame_type)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_raw_acc_data(data: List[int], timestamp: int, frame_type: int) -> dict:
|
|
23
|
+
"""Parse raw (non-compressed) accelerometer data.
|
|
24
|
+
|
|
25
|
+
For raw data, the device sends values in the correct units (milliG),
|
|
26
|
+
so no factor conversion is needed.
|
|
27
|
+
"""
|
|
28
|
+
acc_data = []
|
|
29
|
+
|
|
30
|
+
if frame_type == 0x00: # TYPE_0: 1 byte per axis
|
|
31
|
+
step = 1
|
|
32
|
+
channels = 3
|
|
33
|
+
for i in range(10, len(data), step * channels):
|
|
34
|
+
if i + step * channels <= len(data):
|
|
35
|
+
x = int.from_bytes(data[i : i + step], byteorder="little", signed=True)
|
|
36
|
+
y = int.from_bytes(
|
|
37
|
+
data[i + step : i + 2 * step], byteorder="little", signed=True
|
|
38
|
+
)
|
|
39
|
+
z = int.from_bytes(
|
|
40
|
+
data[i + 2 * step : i + 3 * step], byteorder="little", signed=True
|
|
41
|
+
)
|
|
42
|
+
acc_data.append((x, y, z))
|
|
43
|
+
elif frame_type == 0x01: # TYPE_1: 2 bytes per axis
|
|
44
|
+
step = 2
|
|
45
|
+
channels = 3
|
|
46
|
+
for i in range(10, len(data), step * channels):
|
|
47
|
+
if i + step * channels <= len(data):
|
|
48
|
+
x = int.from_bytes(data[i : i + step], byteorder="little", signed=True)
|
|
49
|
+
y = int.from_bytes(
|
|
50
|
+
data[i + step : i + 2 * step], byteorder="little", signed=True
|
|
51
|
+
)
|
|
52
|
+
z = int.from_bytes(
|
|
53
|
+
data[i + 2 * step : i + 3 * step], byteorder="little", signed=True
|
|
54
|
+
)
|
|
55
|
+
acc_data.append((x, y, z))
|
|
56
|
+
elif frame_type == 0x02: # TYPE_2: 3 bytes per axis
|
|
57
|
+
step = 3
|
|
58
|
+
channels = 3
|
|
59
|
+
for i in range(10, len(data), step * channels):
|
|
60
|
+
if i + step * channels <= len(data):
|
|
61
|
+
x = int.from_bytes(data[i : i + step], byteorder="little", signed=True)
|
|
62
|
+
y = int.from_bytes(
|
|
63
|
+
data[i + step : i + 2 * step], byteorder="little", signed=True
|
|
64
|
+
)
|
|
65
|
+
z = int.from_bytes(
|
|
66
|
+
data[i + 2 * step : i + 3 * step], byteorder="little", signed=True
|
|
67
|
+
)
|
|
68
|
+
acc_data.append((x, y, z))
|
|
69
|
+
|
|
70
|
+
return {"timestamp": timestamp, "data": acc_data}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_compressed_acc_data(
|
|
74
|
+
data: List[int], timestamp: int, frame_type: int, factor: float
|
|
75
|
+
) -> dict:
|
|
76
|
+
"""Parse compressed accelerometer data."""
|
|
77
|
+
if frame_type == 0x00: # Compressed TYPE_0
|
|
78
|
+
# type 0 data arrives in G units, convert to milliG
|
|
79
|
+
acc_factor = factor * 1000
|
|
80
|
+
samples = parse_delta_frames_all(data[10:], 3, 16, "signed_int")
|
|
81
|
+
acc_data = [
|
|
82
|
+
(
|
|
83
|
+
int(sample[0] * acc_factor),
|
|
84
|
+
int(sample[1] * acc_factor),
|
|
85
|
+
int(sample[2] * acc_factor),
|
|
86
|
+
)
|
|
87
|
+
for sample in samples
|
|
88
|
+
]
|
|
89
|
+
elif frame_type == 0x01: # Compressed TYPE_1
|
|
90
|
+
samples = parse_delta_frames_all(data[10:], 3, 16, "signed_int")
|
|
91
|
+
acc_data = [
|
|
92
|
+
(
|
|
93
|
+
int(sample[0] * factor) if factor != 1.0 else sample[0],
|
|
94
|
+
int(sample[1] * factor) if factor != 1.0 else sample[1],
|
|
95
|
+
int(sample[2] * factor) if factor != 1.0 else sample[2],
|
|
96
|
+
)
|
|
97
|
+
for sample in samples
|
|
98
|
+
]
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Unsupported compressed frame type: {frame_type}")
|
|
101
|
+
|
|
102
|
+
return {"timestamp": timestamp, "data": acc_data}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Bluetooth data parsing functions."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
from .. import constants
|
|
5
|
+
from .ecg import parse_ecg_data
|
|
6
|
+
from .accelerometer import parse_acc_data
|
|
7
|
+
from .ppi import parse_ppi_data
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_bluetooth_data(
|
|
11
|
+
data: List[int],
|
|
12
|
+
) -> Union[constants.ECGData, constants.ACCData, constants.PPIData]:
|
|
13
|
+
"""Parse Bluetooth data and return the appropriate data type."""
|
|
14
|
+
try:
|
|
15
|
+
data_type_index = data[0]
|
|
16
|
+
data_type = constants.PMD_MEASUREMENT_TYPES[data_type_index]
|
|
17
|
+
timestamp = (
|
|
18
|
+
int.from_bytes(data[1:9], byteorder="little") + constants.TIMESTAMP_OFFSET
|
|
19
|
+
)
|
|
20
|
+
frame_type = data[9]
|
|
21
|
+
|
|
22
|
+
if data_type == "ECG":
|
|
23
|
+
return parse_ecg_data(data, timestamp)
|
|
24
|
+
elif data_type == "ACC":
|
|
25
|
+
return parse_acc_data(data, timestamp, frame_type)
|
|
26
|
+
elif data_type == "PPI":
|
|
27
|
+
return parse_ppi_data(data, timestamp)
|
|
28
|
+
else:
|
|
29
|
+
print(f"Unsupported data type: {data_type}")
|
|
30
|
+
print(" ".join([f"{byte:02X}" for byte in data]))
|
|
31
|
+
return None
|
|
32
|
+
# raise ValueError(f"Unsupported data type: {data_type}")
|
|
33
|
+
except IndexError as e:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Failed to parse Bluetooth data: insufficient data length"
|
|
36
|
+
) from e
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Common utility functions for parsing operations."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def byte_to_bitmap(byte: int) -> List[bool]:
|
|
7
|
+
"""Convert a byte to a bitmap (list of booleans)."""
|
|
8
|
+
binary_string = f"{byte:08b}"
|
|
9
|
+
reversed_binary_string = binary_string[::-1]
|
|
10
|
+
return [bit == "1" for bit in reversed_binary_string]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Data compression and decompression utilities."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_delta_frames_all(
|
|
8
|
+
data: List[int], channels: int, resolution: int, data_type: str
|
|
9
|
+
) -> List[List[int]]:
|
|
10
|
+
"""Parse delta frames similar to Java's parseDeltaFramesAll method."""
|
|
11
|
+
if len(data) == 0:
|
|
12
|
+
return []
|
|
13
|
+
|
|
14
|
+
offset = 0
|
|
15
|
+
ref_samples = parse_delta_frame_ref_samples(data, channels, resolution, data_type)
|
|
16
|
+
offset += int(channels * math.ceil(resolution / 8.0))
|
|
17
|
+
|
|
18
|
+
samples = [ref_samples]
|
|
19
|
+
|
|
20
|
+
while offset < len(data):
|
|
21
|
+
if offset + 2 > len(data):
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
delta_size = data[offset] & 0xFF
|
|
25
|
+
offset += 1
|
|
26
|
+
sample_count = data[offset] & 0xFF
|
|
27
|
+
offset += 1
|
|
28
|
+
|
|
29
|
+
bit_length = sample_count * delta_size * channels
|
|
30
|
+
length = int(math.ceil(bit_length / 8.0))
|
|
31
|
+
|
|
32
|
+
if offset + length > len(data):
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
delta_frame = data[offset : offset + length]
|
|
36
|
+
delta_samples = parse_delta_frame(delta_frame, channels, delta_size)
|
|
37
|
+
|
|
38
|
+
for delta in delta_samples:
|
|
39
|
+
if len(delta) != channels:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
last_sample = samples[-1]
|
|
43
|
+
next_samples = []
|
|
44
|
+
for i in range(channels):
|
|
45
|
+
sample = last_sample[i] + delta[i]
|
|
46
|
+
next_samples.append(sample)
|
|
47
|
+
samples.append(next_samples)
|
|
48
|
+
|
|
49
|
+
offset += length
|
|
50
|
+
|
|
51
|
+
return samples
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_delta_frame_ref_samples(
|
|
55
|
+
data: List[int], channels: int, resolution: int, data_type: str
|
|
56
|
+
) -> List[int]:
|
|
57
|
+
"""Parse reference samples from delta frame data."""
|
|
58
|
+
samples = []
|
|
59
|
+
offset = 0
|
|
60
|
+
resolution_in_bytes = int(math.ceil(resolution / 8.0))
|
|
61
|
+
|
|
62
|
+
for _ in range(channels):
|
|
63
|
+
if offset + resolution_in_bytes > len(data):
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if data_type == "signed_int":
|
|
67
|
+
sample = int.from_bytes(
|
|
68
|
+
data[offset : offset + resolution_in_bytes],
|
|
69
|
+
byteorder="little",
|
|
70
|
+
signed=True,
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
sample = int.from_bytes(
|
|
74
|
+
data[offset : offset + resolution_in_bytes],
|
|
75
|
+
byteorder="little",
|
|
76
|
+
signed=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
offset += resolution_in_bytes
|
|
80
|
+
samples.append(sample)
|
|
81
|
+
|
|
82
|
+
return samples
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_delta_frame(
|
|
86
|
+
data: List[int], channels: int, bit_width: int
|
|
87
|
+
) -> List[List[int]]:
|
|
88
|
+
"""Parse delta frame data into samples."""
|
|
89
|
+
if len(data) == 0 or bit_width <= 0 or channels <= 0:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
bit_set = []
|
|
93
|
+
for byte_val in data:
|
|
94
|
+
for i in range(8):
|
|
95
|
+
bit_set.append((byte_val >> i) & 1)
|
|
96
|
+
|
|
97
|
+
samples = []
|
|
98
|
+
offset = 0
|
|
99
|
+
|
|
100
|
+
while offset + bit_width * channels <= len(bit_set):
|
|
101
|
+
channel_samples = []
|
|
102
|
+
for _ in range(channels):
|
|
103
|
+
if offset + bit_width > len(bit_set):
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
value = 0
|
|
107
|
+
for i in range(bit_width):
|
|
108
|
+
if offset + i < len(bit_set):
|
|
109
|
+
value |= bit_set[offset + i] << i
|
|
110
|
+
|
|
111
|
+
if bit_width > 1 and (value & (1 << (bit_width - 1))):
|
|
112
|
+
value |= -1 << bit_width
|
|
113
|
+
|
|
114
|
+
channel_samples.append(value)
|
|
115
|
+
offset += bit_width
|
|
116
|
+
|
|
117
|
+
if len(channel_samples) == channels:
|
|
118
|
+
samples.append(channel_samples)
|
|
119
|
+
|
|
120
|
+
return samples
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""ECG (Electrocardiogram) data parsing functions."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from .. import constants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_ecg_data(data: List[int], timestamp: int) -> constants.ECGData:
|
|
8
|
+
"""Parse ECG data from a list of integers."""
|
|
9
|
+
ecg_data = [
|
|
10
|
+
int.from_bytes(data[i : i + 3], byteorder="little", signed=True)
|
|
11
|
+
for i in range(10, len(data), 3)
|
|
12
|
+
]
|
|
13
|
+
return constants.ECGData(timestamp=timestamp, data=ecg_data)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Heart rate data parsing functions."""
|
|
2
|
+
|
|
3
|
+
from .. import constants
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_heartrate_data(data: bytearray) -> constants.HRData:
|
|
7
|
+
"""Parse heart rate data from a bytearray."""
|
|
8
|
+
try:
|
|
9
|
+
heartrate = int.from_bytes(data[1:2], byteorder="little", signed=False)
|
|
10
|
+
rr_intervals = [
|
|
11
|
+
int.from_bytes(data[i : i + 2], byteorder="little", signed=False)
|
|
12
|
+
/ 1024.0
|
|
13
|
+
* 1000.0
|
|
14
|
+
for i in range(2, len(data), 2)
|
|
15
|
+
]
|
|
16
|
+
return constants.HRData(heartrate, rr_intervals)
|
|
17
|
+
except IndexError as e:
|
|
18
|
+
raise ValueError(
|
|
19
|
+
"Failed to parse heart rate data: insufficient data length"
|
|
20
|
+
) from e
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""PMD (Physical Measurement Data) parsing functions."""
|
|
2
|
+
|
|
3
|
+
from .. import constants
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_pmd_data(data: bytearray) -> constants.MeasurementSettings:
|
|
7
|
+
"""Parse PMD data from a bytearray."""
|
|
8
|
+
try:
|
|
9
|
+
measurement_type_index = data[2]
|
|
10
|
+
error_code_index = data[3]
|
|
11
|
+
more_frames = data[4] != 0
|
|
12
|
+
|
|
13
|
+
measurement_type = (
|
|
14
|
+
constants.PMD_MEASUREMENT_TYPES[measurement_type_index]
|
|
15
|
+
if measurement_type_index < len(constants.PMD_MEASUREMENT_TYPES)
|
|
16
|
+
else "UNKNOWN"
|
|
17
|
+
)
|
|
18
|
+
error_code = (
|
|
19
|
+
constants.PMD_CONTROL_POINT_ERROR_CODES[error_code_index]
|
|
20
|
+
if error_code_index < len(constants.PMD_CONTROL_POINT_ERROR_CODES)
|
|
21
|
+
else "UNKNOWN"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
settings = []
|
|
25
|
+
index = 5
|
|
26
|
+
while index < len(data):
|
|
27
|
+
setting_type_index = data[index]
|
|
28
|
+
setting_type = (
|
|
29
|
+
constants.PMD_SETTING_TYPES[setting_type_index]
|
|
30
|
+
if setting_type_index < len(constants.PMD_SETTING_TYPES)
|
|
31
|
+
else "UNKNOWN"
|
|
32
|
+
)
|
|
33
|
+
array_length = data[index + 1]
|
|
34
|
+
field_size = constants.PMD_SETTING_TYPES_TO_FIELD_SIZES.get(setting_type, 2)
|
|
35
|
+
setting_values = []
|
|
36
|
+
for i in range(array_length):
|
|
37
|
+
start_pos = index + 2 + i * field_size
|
|
38
|
+
end_pos = start_pos + field_size
|
|
39
|
+
if end_pos <= len(data):
|
|
40
|
+
if field_size == 1:
|
|
41
|
+
setting_values.append(data[start_pos])
|
|
42
|
+
else:
|
|
43
|
+
setting_values.append(
|
|
44
|
+
int.from_bytes(data[start_pos:end_pos], "little")
|
|
45
|
+
)
|
|
46
|
+
settings.append(
|
|
47
|
+
constants.SettingType(type=setting_type, values=setting_values)
|
|
48
|
+
)
|
|
49
|
+
index += 2 + field_size * array_length
|
|
50
|
+
|
|
51
|
+
return constants.MeasurementSettings(
|
|
52
|
+
measurement_type=measurement_type,
|
|
53
|
+
error_code=error_code,
|
|
54
|
+
more_frames=more_frames,
|
|
55
|
+
settings=settings,
|
|
56
|
+
)
|
|
57
|
+
except IndexError as e:
|
|
58
|
+
raise ValueError("Failed to parse PMD data: insufficient data length") from e
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_measurement_settings(
|
|
62
|
+
measurement_settings: constants.MeasurementSettings,
|
|
63
|
+
) -> bytearray:
|
|
64
|
+
"""Build a bytearray from measurement settings."""
|
|
65
|
+
data = bytearray()
|
|
66
|
+
data.append(constants.PMD_CONTROL_OPERATION_CODE["START"])
|
|
67
|
+
|
|
68
|
+
measurement_type_index = constants.PMD_MEASUREMENT_TYPES.index(
|
|
69
|
+
measurement_settings.measurement_type
|
|
70
|
+
)
|
|
71
|
+
data.append(measurement_type_index)
|
|
72
|
+
|
|
73
|
+
for setting in measurement_settings.settings:
|
|
74
|
+
setting_type_index = constants.PMD_SETTING_TYPES.index(setting.type)
|
|
75
|
+
data.append(setting_type_index)
|
|
76
|
+
data.append(setting.array_length)
|
|
77
|
+
for value in setting.values:
|
|
78
|
+
field_size = constants.PMD_SETTING_TYPES_TO_FIELD_SIZES.get(setting.type, 2)
|
|
79
|
+
data.extend(value.to_bytes(field_size, "little"))
|
|
80
|
+
|
|
81
|
+
return data
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""PPI (Peak-to-Peak Interval) data parsing functions."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from .. import constants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_ppi_data(data: List[int], timestamp: int) -> constants.PPIData:
|
|
8
|
+
"""Parse PPI data from a list of integers."""
|
|
9
|
+
ppi_samples = []
|
|
10
|
+
offset = 10
|
|
11
|
+
|
|
12
|
+
while offset + 6 <= len(data):
|
|
13
|
+
sample = data[offset : offset + 6]
|
|
14
|
+
|
|
15
|
+
hr = sample[0] & 0xFF
|
|
16
|
+
ppi = int.from_bytes(sample[1:3], byteorder="little", signed=False)
|
|
17
|
+
error_estimate = int.from_bytes(sample[3:5], byteorder="little", signed=False)
|
|
18
|
+
status_byte = sample[5] & 0xFF
|
|
19
|
+
|
|
20
|
+
invalid_ppi = (status_byte & 0x01) != 0
|
|
21
|
+
skin_contact_status = (status_byte & 0x02) != 0
|
|
22
|
+
skin_contact_supported = (status_byte & 0x04) != 0
|
|
23
|
+
|
|
24
|
+
ppi_samples.append(
|
|
25
|
+
{
|
|
26
|
+
"ppi": ppi,
|
|
27
|
+
"error_estimate": error_estimate,
|
|
28
|
+
"hr": hr,
|
|
29
|
+
"invalid_ppi": invalid_ppi,
|
|
30
|
+
"skin_contact_status": skin_contact_status,
|
|
31
|
+
"skin_contact_supported": skin_contact_supported,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
offset += 6
|
|
36
|
+
|
|
37
|
+
samples = []
|
|
38
|
+
if timestamp != 0:
|
|
39
|
+
current_timestamp = timestamp
|
|
40
|
+
|
|
41
|
+
for sample in reversed(ppi_samples):
|
|
42
|
+
samples.append(
|
|
43
|
+
constants.PPISample(
|
|
44
|
+
ppi=sample["ppi"],
|
|
45
|
+
error_estimate=sample["error_estimate"],
|
|
46
|
+
hr=sample["hr"],
|
|
47
|
+
invalid_ppi=sample["invalid_ppi"],
|
|
48
|
+
skin_contact_status=sample["skin_contact_status"],
|
|
49
|
+
skin_contact_supported=sample["skin_contact_supported"],
|
|
50
|
+
timestamp=current_timestamp,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
current_timestamp -= sample["ppi"] * 1_000_000
|
|
54
|
+
|
|
55
|
+
samples.reverse()
|
|
56
|
+
else:
|
|
57
|
+
for sample in ppi_samples:
|
|
58
|
+
samples.append(
|
|
59
|
+
constants.PPISample(
|
|
60
|
+
ppi=sample["ppi"],
|
|
61
|
+
error_estimate=sample["error_estimate"],
|
|
62
|
+
hr=sample["hr"],
|
|
63
|
+
invalid_ppi=sample["invalid_ppi"],
|
|
64
|
+
skin_contact_status=sample["skin_contact_status"],
|
|
65
|
+
skin_contact_supported=sample["skin_contact_supported"],
|
|
66
|
+
timestamp=0,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return constants.PPIData(samples=samples)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .parsers import *
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: polar-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: polar-python is a Python library for connecting to Polar devices via Bluetooth Low Energy (BLE) using Bleak. It allows querying device capabilities (e.g., ECG, ACC, PPG), exploring configurable options, and streaming parsed data through callback functions.
|
|
5
5
|
Home-page: https://github.com/zHElEARN/polar-python
|
|
6
6
|
Author: Zhe_Learn
|
|
@@ -16,6 +16,17 @@ Requires-Python: >=3.6
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: bleak
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
19
30
|
|
|
20
31
|
# polar-python
|
|
21
32
|
|
|
@@ -96,8 +107,8 @@ async def main():
|
|
|
96
107
|
ecg_settings = MeasurementSettings(
|
|
97
108
|
measurement_type="ECG",
|
|
98
109
|
settings=[
|
|
99
|
-
SettingType(type="SAMPLE_RATE",
|
|
100
|
-
SettingType(type="RESOLUTION",
|
|
110
|
+
SettingType(type="SAMPLE_RATE", values=[130]),
|
|
111
|
+
SettingType(type="RESOLUTION", values=[14]),
|
|
101
112
|
],
|
|
102
113
|
)
|
|
103
114
|
|
|
@@ -105,9 +116,9 @@ async def main():
|
|
|
105
116
|
acc_settings = MeasurementSettings(
|
|
106
117
|
measurement_type="ACC",
|
|
107
118
|
settings=[
|
|
108
|
-
SettingType(type="SAMPLE_RATE",
|
|
109
|
-
SettingType(type="RESOLUTION",
|
|
110
|
-
SettingType(type="RANGE",
|
|
119
|
+
SettingType(type="SAMPLE_RATE", values=[25]),
|
|
120
|
+
SettingType(type="RESOLUTION", values=[16]),
|
|
121
|
+
SettingType(type="RANGE", values=[2]),
|
|
111
122
|
],
|
|
112
123
|
)
|
|
113
124
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
polar_python/__init__.py
|
|
5
|
+
polar_python/constants.py
|
|
6
|
+
polar_python/device.py
|
|
7
|
+
polar_python/exceptions.py
|
|
8
|
+
polar_python/utils.py
|
|
9
|
+
polar_python.egg-info/PKG-INFO
|
|
10
|
+
polar_python.egg-info/SOURCES.txt
|
|
11
|
+
polar_python.egg-info/dependency_links.txt
|
|
12
|
+
polar_python.egg-info/requires.txt
|
|
13
|
+
polar_python.egg-info/top_level.txt
|
|
14
|
+
polar_python/parsers/__init__.py
|
|
15
|
+
polar_python/parsers/accelerometer.py
|
|
16
|
+
polar_python/parsers/bluetooth.py
|
|
17
|
+
polar_python/parsers/common.py
|
|
18
|
+
polar_python/parsers/compression.py
|
|
19
|
+
polar_python/parsers/ecg.py
|
|
20
|
+
polar_python/parsers/heartrate.py
|
|
21
|
+
polar_python/parsers/pmd.py
|
|
22
|
+
polar_python/parsers/ppi.py
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
from typing import List, Tuple, Union
|
|
2
|
-
from . import constants
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def byte_to_bitmap(byte: int) -> List[bool]:
|
|
6
|
-
"""Convert a byte to a bitmap (list of booleans)."""
|
|
7
|
-
binary_string = f"{byte:08b}"
|
|
8
|
-
reversed_binary_string = binary_string[::-1]
|
|
9
|
-
return [bit == "1" for bit in reversed_binary_string]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def parse_pmd_data(data: bytearray) -> constants.MeasurementSettings:
|
|
13
|
-
"""Parse PMD data from a bytearray."""
|
|
14
|
-
try:
|
|
15
|
-
measurement_type_index = data[2]
|
|
16
|
-
error_code_index = data[3]
|
|
17
|
-
more_frames = data[4] != 0
|
|
18
|
-
|
|
19
|
-
measurement_type = (
|
|
20
|
-
constants.PMD_MEASUREMENT_TYPES[measurement_type_index]
|
|
21
|
-
if measurement_type_index < len(constants.PMD_MEASUREMENT_TYPES)
|
|
22
|
-
else "UNKNOWN"
|
|
23
|
-
)
|
|
24
|
-
error_code = (
|
|
25
|
-
constants.PMD_CONTROL_POINT_ERROR_CODES[error_code_index]
|
|
26
|
-
if error_code_index < len(constants.PMD_CONTROL_POINT_ERROR_CODES)
|
|
27
|
-
else "UNKNOWN"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
settings = []
|
|
31
|
-
index = 5
|
|
32
|
-
while index < len(data):
|
|
33
|
-
setting_type_index = data[index]
|
|
34
|
-
setting_type = (
|
|
35
|
-
constants.PMD_SETTING_TYPES[setting_type_index]
|
|
36
|
-
if setting_type_index < len(constants.PMD_SETTING_TYPES)
|
|
37
|
-
else "UNKNOWN"
|
|
38
|
-
)
|
|
39
|
-
array_length = data[index + 1]
|
|
40
|
-
setting_values = [
|
|
41
|
-
int.from_bytes(data[index + 2 + 2 * i : index + 4 + 2 * i], "little")
|
|
42
|
-
for i in range(array_length)
|
|
43
|
-
]
|
|
44
|
-
settings.append(
|
|
45
|
-
constants.SettingType(
|
|
46
|
-
type=setting_type, array_length=array_length, values=setting_values
|
|
47
|
-
)
|
|
48
|
-
)
|
|
49
|
-
index += 2 + 2 * array_length
|
|
50
|
-
|
|
51
|
-
return constants.MeasurementSettings(
|
|
52
|
-
measurement_type=measurement_type,
|
|
53
|
-
error_code=error_code,
|
|
54
|
-
more_frames=more_frames,
|
|
55
|
-
settings=settings,
|
|
56
|
-
)
|
|
57
|
-
except IndexError as e:
|
|
58
|
-
raise ValueError("Failed to parse PMD data: insufficient data length") from e
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def build_measurement_settings(
|
|
62
|
-
measurement_settings: constants.MeasurementSettings,
|
|
63
|
-
) -> bytearray:
|
|
64
|
-
"""Build a bytearray from measurement settings."""
|
|
65
|
-
data = bytearray()
|
|
66
|
-
data.append(constants.PMD_CONTROL_OPERATION_CODE["START"])
|
|
67
|
-
|
|
68
|
-
measurement_type_index = constants.PMD_MEASUREMENT_TYPES.index(
|
|
69
|
-
measurement_settings.measurement_type
|
|
70
|
-
)
|
|
71
|
-
data.append(measurement_type_index)
|
|
72
|
-
|
|
73
|
-
for setting in measurement_settings.settings:
|
|
74
|
-
setting_type_index = constants.PMD_SETTING_TYPES.index(setting.type)
|
|
75
|
-
data.append(setting_type_index)
|
|
76
|
-
data.append(setting.array_length)
|
|
77
|
-
for value in setting.values:
|
|
78
|
-
data.extend(value.to_bytes(2, "little"))
|
|
79
|
-
|
|
80
|
-
return data
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def parse_ecg_data(data: List[int], timestamp: int) -> constants.ECGData:
|
|
84
|
-
"""Parse ECG data from a list of integers."""
|
|
85
|
-
ecg_data = [
|
|
86
|
-
int.from_bytes(data[i : i + 3], byteorder="little", signed=True)
|
|
87
|
-
for i in range(10, len(data), 3)
|
|
88
|
-
]
|
|
89
|
-
return constants.ECGData(timestamp=timestamp, data=ecg_data)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def parse_acc_data(
|
|
93
|
-
data: List[int], timestamp: int, frame_type: int
|
|
94
|
-
) -> constants.ACCData:
|
|
95
|
-
"""Parse accelerometer data from a list of integers based on frame type."""
|
|
96
|
-
acc_data = []
|
|
97
|
-
if frame_type == 0x00:
|
|
98
|
-
acc_data = [
|
|
99
|
-
(
|
|
100
|
-
int.from_bytes(data[i : i + 1], byteorder="little", signed=True),
|
|
101
|
-
int.from_bytes(data[i + 1 : i + 2], byteorder="little", signed=True),
|
|
102
|
-
int.from_bytes(data[i + 2 : i + 3], byteorder="little", signed=True),
|
|
103
|
-
)
|
|
104
|
-
for i in range(10, len(data), 3)
|
|
105
|
-
]
|
|
106
|
-
elif frame_type == 0x01:
|
|
107
|
-
acc_data = [
|
|
108
|
-
(
|
|
109
|
-
int.from_bytes(data[i : i + 2], byteorder="little", signed=True),
|
|
110
|
-
int.from_bytes(data[i + 2 : i + 4], byteorder="little", signed=True),
|
|
111
|
-
int.from_bytes(data[i + 4 : i + 6], byteorder="little", signed=True),
|
|
112
|
-
)
|
|
113
|
-
for i in range(10, len(data), 6)
|
|
114
|
-
]
|
|
115
|
-
elif frame_type == 0x02:
|
|
116
|
-
acc_data = [
|
|
117
|
-
(
|
|
118
|
-
int.from_bytes(data[i : i + 3], byteorder="little", signed=True),
|
|
119
|
-
int.from_bytes(data[i + 3 : i + 6], byteorder="little", signed=True),
|
|
120
|
-
int.from_bytes(data[i + 6 : i + 9], byteorder="little", signed=True),
|
|
121
|
-
)
|
|
122
|
-
for i in range(10, len(data), 9)
|
|
123
|
-
]
|
|
124
|
-
return constants.ACCData(timestamp=timestamp, data=acc_data)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def parse_bluetooth_data(
|
|
128
|
-
data: List[int],
|
|
129
|
-
) -> Union[constants.ECGData, constants.ACCData]:
|
|
130
|
-
"""Parse Bluetooth data and return the appropriate data type."""
|
|
131
|
-
try:
|
|
132
|
-
data_type_index = data[0]
|
|
133
|
-
data_type = constants.PMD_MEASUREMENT_TYPES[data_type_index]
|
|
134
|
-
timestamp = (
|
|
135
|
-
int.from_bytes(data[1:9], byteorder="little") + constants.TIMESTAMP_OFFSET
|
|
136
|
-
)
|
|
137
|
-
frame_type = data[9]
|
|
138
|
-
|
|
139
|
-
if data_type == "ECG":
|
|
140
|
-
return parse_ecg_data(data, timestamp)
|
|
141
|
-
elif data_type == "ACC":
|
|
142
|
-
return parse_acc_data(data, timestamp, frame_type)
|
|
143
|
-
else:
|
|
144
|
-
raise ValueError(f"Unsupported data type: {data_type}")
|
|
145
|
-
except IndexError as e:
|
|
146
|
-
raise ValueError(
|
|
147
|
-
"Failed to parse Bluetooth data: insufficient data length"
|
|
148
|
-
) from e
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def parse_heartrate_data(data: bytearray) -> constants.HRData:
|
|
152
|
-
"""Parse heart rate data from a bytearray."""
|
|
153
|
-
try:
|
|
154
|
-
heartrate = int.from_bytes(data[1:2], byteorder="little", signed=False)
|
|
155
|
-
rr_intervals = [
|
|
156
|
-
int.from_bytes(data[i : i + 2], byteorder="little", signed=False)
|
|
157
|
-
/ 1024.0
|
|
158
|
-
* 1000.0
|
|
159
|
-
for i in range(2, len(data), 2)
|
|
160
|
-
]
|
|
161
|
-
return constants.HRData(heartrate, rr_intervals)
|
|
162
|
-
except IndexError as e:
|
|
163
|
-
raise ValueError(
|
|
164
|
-
"Failed to parse heart rate data: insufficient data length"
|
|
165
|
-
) from e
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
setup.py
|
|
4
|
-
polar_python/__init__.py
|
|
5
|
-
polar_python/constants.py
|
|
6
|
-
polar_python/device.py
|
|
7
|
-
polar_python/exceptions.py
|
|
8
|
-
polar_python/utils.py
|
|
9
|
-
polar_python.egg-info/PKG-INFO
|
|
10
|
-
polar_python.egg-info/SOURCES.txt
|
|
11
|
-
polar_python.egg-info/dependency_links.txt
|
|
12
|
-
polar_python.egg-info/requires.txt
|
|
13
|
-
polar_python.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|