polar-python 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ from .device import PolarDevice
2
+ from .constants import MeasurementSettings, SettingType, ECGData, ACCData, HRData
3
+
4
+ __all__ = [
5
+ "PolarDevice",
6
+ "MeasurementSettings",
7
+ "SettingType",
8
+ "ECGData",
9
+ "ACCData",
10
+ "HRData",
11
+ ]
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Tuple
3
+
4
+ # UUIDs for Polar device characteristics
5
+ HEART_RATE_CHAR_UUID: str = "00002a37-0000-1000-8000-00805f9b34fb"
6
+ PMD_CONTROL_POINT_UUID: str = "FB005C81-02E7-F387-1CAD-8ACD2D8DF0C8"
7
+ PMD_DATA_UUID: str = "FB005C82-02E7-F387-1CAD-8ACD2D8DF0C8"
8
+
9
+ # PMD Measurement Types
10
+ PMD_MEASUREMENT_TYPES: List[str] = ["ECG", "PPG", "ACC", "PPI", "RFU", "GYRO", "MAG"]
11
+
12
+ # PMD Control Point Error Codes
13
+ PMD_CONTROL_POINT_ERROR_CODES: List[str] = [
14
+ "SUCCESS",
15
+ "ERROR INVALID OP CODE",
16
+ "ERROR INVALID MEASUREMENT TYPE",
17
+ "ERROR NOT SUPPORTED",
18
+ "ERROR INVALID LENGTH",
19
+ "ERROR INVALID PARAMETER",
20
+ "ERROR ALREADY IN STATE",
21
+ "ERROR INVALID RESOLUTION",
22
+ "ERROR INVALID SAMPLE RATE",
23
+ "ERROR INVALID RANGE",
24
+ "ERROR INVALID MTU",
25
+ "ERROR INVALID NUMBER OF CHANNELS",
26
+ "ERROR INVALID STATE",
27
+ "ERROR DEVICE IN CHARGER",
28
+ ]
29
+
30
+ # PMD Control Operation Codes
31
+ PMD_CONTROL_OPERATION_CODE: dict = {"GET": 0x01, "START": 0x02, "STOP": 0x03}
32
+
33
+ # PMD Setting Types
34
+ PMD_SETTING_TYPES: List[str] = ["SAMPLE_RATE", "RESOLUTION", "RANGE", "RFU", "CHANNELS"]
35
+
36
+ # Timestamp Offset
37
+ TIMESTAMP_OFFSET: int = 946684800000000000
38
+
39
+
40
+ @dataclass
41
+ class SettingType:
42
+ """Represents a setting type with its array length and possible values."""
43
+
44
+ type: str
45
+ array_length: int
46
+ values: List[int]
47
+
48
+
49
+ @dataclass
50
+ class MeasurementSettings:
51
+ """Represents measurement settings for a specific type."""
52
+
53
+ measurement_type: str
54
+ settings: List[SettingType]
55
+ error_code: Optional[str] = None
56
+ more_frames: Optional[bool] = None
57
+
58
+
59
+ @dataclass
60
+ class ACCData:
61
+ """Represents accelerometer data."""
62
+
63
+ timestamp: int
64
+ data: List[Tuple[int, int, int]]
65
+
66
+
67
+ @dataclass
68
+ class ECGData:
69
+ """Represents ECG data."""
70
+
71
+ timestamp: int
72
+ data: List[int]
73
+
74
+
75
+ @dataclass
76
+ class HRData:
77
+ """Represents heart rate data."""
78
+
79
+ heartrate: int
80
+ rr_intervals: List[float]
polar_python/device.py ADDED
@@ -0,0 +1,171 @@
1
+ import asyncio
2
+ from bleak import BleakClient
3
+ from bleak.backends.device import BLEDevice
4
+ from bleak.backends.characteristic import BleakGATTCharacteristic
5
+ from typing import Union, Callable, List
6
+
7
+ from . import constants, exceptions, utils
8
+
9
+
10
+ class PolarDevice:
11
+ def __init__(
12
+ self,
13
+ address_or_ble_device: Union[str, BLEDevice],
14
+ data_callback: Callable[
15
+ [Union[constants.ECGData, constants.ACCData]], None
16
+ ] = None,
17
+ heartrate_callback: Callable[[constants.HRData], None] = None,
18
+ ) -> None:
19
+ """
20
+ Initialize the PolarDevice with a BLE address or device.
21
+
22
+ :param address_or_ble_device: The address or BLEDevice instance of the Polar device.
23
+ :param data_callback: Callback function to handle data streams.
24
+ :param heartrate_callback: Callback function to handle heart rate data.
25
+ """
26
+ self.client = BleakClient(address_or_ble_device)
27
+ self._queue_pmd_control = asyncio.Queue()
28
+ self._data_callback = data_callback
29
+ self._heartrate_callback = heartrate_callback
30
+
31
+ async def connect(self) -> None:
32
+ """Connect to the Polar device."""
33
+ try:
34
+ await self.client.connect()
35
+ await self.client.start_notify(
36
+ constants.PMD_CONTROL_POINT_UUID, self._handle_pmd_control
37
+ )
38
+ await self.client.start_notify(
39
+ constants.PMD_DATA_UUID, self._handle_pmd_data
40
+ )
41
+ except Exception as e:
42
+ raise exceptions.ConnectionError(
43
+ f"Failed to connect to the Polar device: {str(e)}"
44
+ ) from e
45
+
46
+ async def disconnect(self) -> None:
47
+ """Disconnect from the Polar device."""
48
+ try:
49
+ await self.client.disconnect()
50
+ except Exception as e:
51
+ raise exceptions.DisconnectionError(
52
+ f"Failed to disconnect from the Polar device: {str(e)}"
53
+ ) from e
54
+
55
+ async def __aenter__(self):
56
+ """Support for async context management."""
57
+ await self.connect()
58
+ return self
59
+
60
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
61
+ """Support for async context management."""
62
+ await self.disconnect()
63
+
64
+ async def available_features(self) -> List[str]:
65
+ """Retrieve available features from the Polar device."""
66
+ try:
67
+ data = await self.client.read_gatt_char(constants.PMD_CONTROL_POINT_UUID)
68
+ if data[0] != 0x0F:
69
+ raise exceptions.ControlPointResponseError(
70
+ "Unexpected response from the control point"
71
+ )
72
+ features = data[1]
73
+ bitmap = utils.byte_to_bitmap(features)
74
+ return [
75
+ constants.PMD_MEASUREMENT_TYPES[int(index)]
76
+ for index, bit in enumerate(bitmap)
77
+ if bit
78
+ ]
79
+ except Exception as e:
80
+ raise exceptions.ReadCharacteristicError(
81
+ f"Failed to read available features: {str(e)}"
82
+ ) from e
83
+
84
+ async def request_stream_settings(
85
+ self, measurement_type: str
86
+ ) -> constants.MeasurementSettings:
87
+ """Request stream settings for a specific measurement type."""
88
+ try:
89
+ await self.client.write_gatt_char(
90
+ constants.PMD_CONTROL_POINT_UUID,
91
+ bytearray(
92
+ [
93
+ constants.PMD_CONTROL_OPERATION_CODE["GET"],
94
+ constants.PMD_MEASUREMENT_TYPES.index(measurement_type),
95
+ ]
96
+ ),
97
+ )
98
+ return utils.parse_pmd_data(await self._queue_pmd_control.get())
99
+ except Exception as e:
100
+ raise exceptions.StreamSettingsError(
101
+ f"Failed to request stream settings for {measurement_type}: {str(e)}"
102
+ ) from e
103
+
104
+ async def start_stream(self, settings: constants.MeasurementSettings) -> None:
105
+ """Start data stream with specified settings."""
106
+ try:
107
+ data = utils.build_measurement_settings(settings)
108
+ await self.client.write_gatt_char(constants.PMD_CONTROL_POINT_UUID, data)
109
+ except Exception as e:
110
+ raise exceptions.WriteCharacteristicError(
111
+ f"Failed to start stream with settings {settings}: {str(e)}"
112
+ ) from e
113
+
114
+ async def stop_stream(self, measurement_type: str) -> None:
115
+ """Stop data stream for a specific measurement type."""
116
+ try:
117
+ await self.client.write_gatt_char(
118
+ constants.PMD_CONTROL_POINT_UUID,
119
+ bytearray(
120
+ [
121
+ constants.PMD_CONTROL_OPERATION_CODE["STOP"],
122
+ constants.PMD_MEASUREMENT_TYPES.index(measurement_type),
123
+ ]
124
+ ),
125
+ )
126
+ except Exception as e:
127
+ raise exceptions.WriteCharacteristicError(
128
+ f"Failed to stop stream for {measurement_type}: {str(e)}"
129
+ ) from e
130
+
131
+ async def start_heartrate_stream(self) -> None:
132
+ """Start heart rate data stream."""
133
+ try:
134
+ await self.client.start_notify(
135
+ constants.HEART_RATE_CHAR_UUID, self._handle_heartrate_measurement
136
+ )
137
+ except Exception as e:
138
+ raise exceptions.NotificationError(
139
+ f"Failed to start heart rate stream: {str(e)}"
140
+ ) from e
141
+
142
+ async def stop_heartrate_stream(self) -> None:
143
+ """Stop heart rate data stream."""
144
+ try:
145
+ await self.client.stop_notify(constants.HEART_RATE_CHAR_UUID)
146
+ except Exception as e:
147
+ raise exceptions.NotificationError(
148
+ f"Failed to stop heart rate stream: {str(e)}"
149
+ ) from e
150
+
151
+ def _handle_pmd_control(
152
+ self, sender: BleakGATTCharacteristic, data: bytearray
153
+ ) -> None:
154
+ """Handle PMD control notifications."""
155
+ self._queue_pmd_control.put_nowait(data)
156
+
157
+ def _handle_pmd_data(
158
+ self, sender: BleakGATTCharacteristic, data: bytearray
159
+ ) -> None:
160
+ """Handle PMD data notifications."""
161
+ parsed_data = utils.parse_bluetooth_data(data)
162
+ if self._data_callback:
163
+ self._data_callback(parsed_data)
164
+
165
+ def _handle_heartrate_measurement(
166
+ self, sender: BleakGATTCharacteristic, data: bytearray
167
+ ) -> None:
168
+ """Handle heart rate measurement notifications."""
169
+ parsed_data = utils.parse_heartrate_data(data)
170
+ if self._heartrate_callback:
171
+ self._heartrate_callback(parsed_data)
@@ -0,0 +1,58 @@
1
+ class PolarPythonError(Exception):
2
+ pass
3
+
4
+
5
+ class ControlPointResponseError(PolarPythonError):
6
+ """Exception raised when there is an unexpected response from the control point."""
7
+
8
+ def __init__(self, message="Unexpected response from the control point"):
9
+ self.message = message
10
+ super().__init__(self.message)
11
+
12
+
13
+ class ConnectionError(PolarPythonError):
14
+ """Exception raised when the device fails to connect."""
15
+
16
+ def __init__(self, message="Failed to connect to the Polar device"):
17
+ self.message = message
18
+ super().__init__(self.message)
19
+
20
+
21
+ class DisconnectionError(PolarPythonError):
22
+ """Exception raised when the device fails to disconnect."""
23
+
24
+ def __init__(self, message="Failed to disconnect from the Polar device"):
25
+ self.message = message
26
+ super().__init__(self.message)
27
+
28
+
29
+ class NotificationError(PolarPythonError):
30
+ """Exception raised when notifications cannot be started or stopped."""
31
+
32
+ def __init__(self, message="Failed to start or stop notifications"):
33
+ self.message = message
34
+ super().__init__(self.message)
35
+
36
+
37
+ class ReadCharacteristicError(PolarPythonError):
38
+ """Exception raised when a GATT characteristic cannot be read."""
39
+
40
+ def __init__(self, message="Failed to read GATT characteristic"):
41
+ self.message = message
42
+ super().__init__(self.message)
43
+
44
+
45
+ class WriteCharacteristicError(PolarPythonError):
46
+ """Exception raised when a GATT characteristic cannot be written."""
47
+
48
+ def __init__(self, message="Failed to write GATT characteristic"):
49
+ self.message = message
50
+ super().__init__(self.message)
51
+
52
+
53
+ class StreamSettingsError(PolarPythonError):
54
+ """Exception raised when stream settings are invalid or cannot be set."""
55
+
56
+ def __init__(self, message="Invalid or failed to set stream settings"):
57
+ self.message = message
58
+ super().__init__(self.message)
polar_python/utils.py ADDED
@@ -0,0 +1,165 @@
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
+ * 1024.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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Zachary Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.1
2
+ Name: polar-python
3
+ Version: 0.0.1
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
+ Home-page: https://github.com/zHElEARN/polar-python
6
+ Author: Zhe_Learn
7
+ Author-email: personal@zhelearn.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.6
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: bleak
19
+
20
+ # polar-python
21
+
22
+ `polar-python` is a Python library designed for seamless integration with Polar devices using Bluetooth Low Energy (BLE) through the Bleak library. With `polar-python`, you can easily connect to Polar devices, query supported functionalities such as ECG, ACC, and PPG, explore configurable options and their possible values, and start data streaming to receive parsed binary data through callback functions.
23
+
24
+ ## Features
25
+
26
+ - **Connect to Polar Devices**: Use BLE to connect to Polar devices.
27
+ - **Query Device Capabilities**: Discover supported functionalities like ECG, ACC, and PPG.
28
+ - **Explore Configurable Options**: Query and set measurement settings for each feature.
29
+ - **Stream Data**: Start data streaming and receive parsed binary data via callback functions.
30
+
31
+ ## Installation
32
+
33
+ Since `polar-python` is not yet available on PyPI, you can install it locally by following these steps:
34
+
35
+ 1. Clone the repository:
36
+
37
+ ```sh
38
+ git clone https://github.com/yourusername/polar-python.git
39
+ ```
40
+
41
+ 2. Navigate to the project directory:
42
+
43
+ ```sh
44
+ cd polar-python
45
+ ```
46
+
47
+ 3. Install the package using pip:
48
+
49
+ ```sh
50
+ pip install .
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ Below is an example of how to use `polar-python` to connect to a Polar device, query its features, set measurement settings, and start data streaming.
56
+
57
+ ### Step 1: Import Libraries and Initialize Console
58
+
59
+ ```python
60
+ import asyncio
61
+ from bleak import BleakScanner
62
+ from polar_python import PolarDevice, MeasurementSettings, SettingType, ECGData, ACCData
63
+ ```
64
+
65
+ ### Step 2: Define Data Callback Function
66
+
67
+ ```python
68
+ def data_callback(data: Union[ECGData, ACCData]):
69
+ """
70
+ Callback function to handle incoming data from the Polar device.
71
+
72
+ Args:
73
+ data (Union[ECGData, ACCData]): The data received from the Polar device.
74
+ """
75
+ ```
76
+
77
+ ### Step 3: Define Main Function to Connect to Polar Device
78
+
79
+ ```python
80
+ async def main():
81
+ """
82
+ Main function to connect to a Polar device, query its features,
83
+ set measurement settings, and start data streaming.
84
+ """
85
+ # Find the Polar H10 device
86
+ device = await BleakScanner.find_device_by_filter(
87
+ lambda bd, ad: bd.name and "Polar H10" in bd.name, timeout=5
88
+ )
89
+ if device is None:
90
+ return
91
+ ```
92
+
93
+ ### Step 4: Connect to Polar Device and Query Features
94
+
95
+ ```python
96
+ # Establish connection to the Polar device
97
+ async with PolarDevice(device, data_callback) as polar_device:
98
+ # Query available features
99
+ available_features = await polar_device.available_features()
100
+
101
+ # Query and print stream settings for each feature
102
+ for feature in available_features:
103
+ settings = await polar_device.request_stream_settings(feature)
104
+ ```
105
+
106
+ ### Step 5: Define and Start Data Streams
107
+
108
+ ```python
109
+ # Define ECG measurement settings
110
+ ecg_settings = MeasurementSettings(
111
+ measurement_type="ECG",
112
+ settings=[
113
+ SettingType(type="SAMPLE_RATE", array_length=1, values=[130]),
114
+ SettingType(type="RESOLUTION", array_length=1, values=[14]),
115
+ ],
116
+ )
117
+
118
+ # Define ACC measurement settings
119
+ acc_settings = MeasurementSettings(
120
+ measurement_type="ACC",
121
+ settings=[
122
+ SettingType(type="SAMPLE_RATE", array_length=1, values=[25]),
123
+ SettingType(type="RESOLUTION", array_length=1, values=[16]),
124
+ SettingType(type="RANGE", array_length=1, values=[2]),
125
+ ],
126
+ )
127
+
128
+ # Start data streams for ECG and ACC
129
+ await polar_device.start_stream(ecg_settings)
130
+ await polar_device.start_stream(acc_settings)
131
+
132
+ # Keep the stream running for 120 seconds
133
+ await asyncio.sleep(120)
134
+ ```
135
+
136
+ ### Step 6: Run the Main Function
137
+
138
+ ```python
139
+ if __name__ == "__main__":
140
+ asyncio.run(main())
141
+ ```
142
+
143
+ ## License
144
+
145
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
146
+
147
+ ## Acknowledgements
148
+
149
+ - [Bleak](https://github.com/hbldh/bleak) - BLE library for Python.
150
+ - [Rich](https://github.com/Textualize/rich) - Python library for rich text and beautiful formatting in the terminal.
151
+ - [bleakheart](https://github.com/fsmeraldi/bleakheart) - For providing inspiration and valuable insights.
152
+ - [Polar BLE SDK](https://github.com/polarofficial/polar-ble-sdk) - For providing official BLE SDK and documentation for Polar devices.
@@ -0,0 +1,10 @@
1
+ polar_python/__init__.py,sha256=Fbic2HAqyswlP1NYVg8fYHX6s0ydpZuo3KEJBlMSpOE,238
2
+ polar_python/constants.py,sha256=-rgr_lJ2YjLCPx7ClI522oRMjdJ5o3f8uMODj3h3-u4,1947
3
+ polar_python/device.py,sha256=6kv29xO9NNXjj_y56TZR0dnW8MaFg-nA5gEtO_dSLrQ,6592
4
+ polar_python/exceptions.py,sha256=W_ijsakQDNqPmmFkA_xj6y7I2h38tvV0obpH5FaJvVQ,1907
5
+ polar_python/utils.py,sha256=VvaN_6fSRAlppjcRw8MghIY8rskWldpkWATJbhQi11g,5972
6
+ polar_python-0.0.1.dist-info/LICENSE,sha256=0GAl0AmKuCcFbLXj8gfjjdKOD_bLyyJFTwNG2VkG_E4,1068
7
+ polar_python-0.0.1.dist-info/METADATA,sha256=LZutQ_aZQ6rJyH2WYsHCaMv-NIfFrIX4vjt8NT7nH-E,5262
8
+ polar_python-0.0.1.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
9
+ polar_python-0.0.1.dist-info/top_level.txt,sha256=Gj46AvJ0ljNKmSSJGdOcnm49yH391rcZW80AABcibhA,13
10
+ polar_python-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (71.0.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ polar_python