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.
Files changed (26) hide show
  1. {polar_python-0.0.4 → polar_python-0.0.5}/PKG-INFO +18 -7
  2. {polar_python-0.0.4 → polar_python-0.0.5}/README.md +5 -5
  3. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/constants.py +45 -2
  4. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/device.py +2 -2
  5. polar_python-0.0.5/polar_python/parsers/__init__.py +37 -0
  6. polar_python-0.0.5/polar_python/parsers/accelerometer.py +102 -0
  7. polar_python-0.0.5/polar_python/parsers/bluetooth.py +36 -0
  8. polar_python-0.0.5/polar_python/parsers/common.py +10 -0
  9. polar_python-0.0.5/polar_python/parsers/compression.py +120 -0
  10. polar_python-0.0.5/polar_python/parsers/ecg.py +13 -0
  11. polar_python-0.0.5/polar_python/parsers/heartrate.py +20 -0
  12. polar_python-0.0.5/polar_python/parsers/pmd.py +81 -0
  13. polar_python-0.0.5/polar_python/parsers/ppi.py +70 -0
  14. polar_python-0.0.5/polar_python/utils.py +1 -0
  15. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/PKG-INFO +18 -7
  16. polar_python-0.0.5/polar_python.egg-info/SOURCES.txt +22 -0
  17. {polar_python-0.0.4 → polar_python-0.0.5}/setup.py +1 -1
  18. polar_python-0.0.4/polar_python/utils.py +0 -165
  19. polar_python-0.0.4/polar_python.egg-info/SOURCES.txt +0 -13
  20. {polar_python-0.0.4 → polar_python-0.0.5}/LICENSE +0 -0
  21. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/__init__.py +0 -0
  22. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python/exceptions.py +0 -0
  23. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/dependency_links.txt +0 -0
  24. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/requires.txt +0 -0
  25. {polar_python-0.0.4 → polar_python-0.0.5}/polar_python.egg-info/top_level.txt +0 -0
  26. {polar_python-0.0.4 → polar_python-0.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: polar-python
3
- Version: 0.0.4
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", array_length=1, values=[130]),
100
- SettingType(type="RESOLUTION", array_length=1, values=[14]),
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", array_length=1, values=[25]),
109
- SettingType(type="RESOLUTION", array_length=1, values=[16]),
110
- SettingType(type="RANGE", array_length=1, values=[2]),
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", array_length=1, values=[130]),
81
- SettingType(type="RESOLUTION", array_length=1, values=[14]),
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", array_length=1, values=[25]),
90
- SettingType(type="RESOLUTION", array_length=1, values=[16]),
91
- SettingType(type="RANGE", array_length=1, values=[2]),
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] = ["SAMPLE_RATE", "RESOLUTION", "RANGE", "RFU", "CHANNELS"]
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
1
+ Metadata-Version: 2.4
2
2
  Name: polar-python
3
- Version: 0.0.4
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", array_length=1, values=[130]),
100
- SettingType(type="RESOLUTION", array_length=1, values=[14]),
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", array_length=1, values=[25]),
109
- SettingType(type="RESOLUTION", array_length=1, values=[16]),
110
- SettingType(type="RANGE", array_length=1, values=[2]),
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
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="polar-python",
8
- version="0.0.4",
8
+ version="0.0.5",
9
9
  packages=find_packages(),
10
10
  install_requires=["bleak"],
11
11
  author="Zhe_Learn",
@@ -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