Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ public enum BoardIds
IRONBCI_32_BOARD = 65,
NEUROPAWN_KNIGHT_BOARD_IMU = 66,
MUSE_S_ATHENA_BOARD = 67,
SHIMMER3_BOARD = 68
SHIMMER3_BOARD = 68,
POLAR_H10_BOARD = 73
};


Expand Down
62 changes: 62 additions & 0 deletions docs/SupportedBoards.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1581,3 +1581,65 @@ Common sensor bits:
- :code:`0x000008`, EXG2 24-bit
- :code:`0x000004`, GSR, exposed as EDA
- :code:`0x002000`, battery

Polar H10
---------

``BoardIds.POLAR_H10_BOARD`` (73) uses Bluetooth LE through SimpleBLE to acquire
H10 ECG at 130 Hz and Heart Rate Service notifications. Wear the strap to wake
it and enable skin contact. ``mac_address`` can select its BLE address;
``serial_number`` can select the device ID at the end of its advertised
``Polar H10 <ID>`` name (useful on macOS). Without either, the first matching
H10 found by adapter 0 is used. ``timeout`` controls discovery, PMD replies and
first-ECG waiting in seconds (default 5, maximum 60). Use a BrainFlow distribution
built with BLE support and grant the operating system's Bluetooth permissions.

Preparation subscribes to PMD control indications (or notifications if advertised),
PMD data and HR notifications, then verifies the device offers 130 Hz / 14-bit
ECG. Acquisition waits for a successful PMD START reply and a valid ECG packet.
PMD errors are returned to the caller. H9, Verity Sense, compressed PMD frames,
accelerometry, offline recordings and ECG settings other than this profile are
not supported by this initial driver.

The default preset contains ECG in microvolts. Its ``other_channels`` contains
the PMD device timestamp in seconds, interpolated from the packet's last sample.
The device clock is not assumed to be Unix time. The BrainFlow timestamp anchors
the last sample to host receive time, with samples spaced at 130 Hz. Battery is
a preparation-time snapshot in percent, or NaN when unavailable. The package
counter is generated by the host. Duplicate/non-increasing PMD timestamps are
ignored; reprepare the session if the device clock resets.

The **auxiliary preset is irregular**, so its declared sampling rate is **0**.
Its ``other_channels`` contains heart rate (beats/minute), RR interval (milliseconds)
and contact state (0/1, or NaN if the device does not report contact). Every RR
interval is emitted separately, including multiple intervals in one notification.
A notification without RR values emits one row with NaN for RR. Timestamps are
host arrival estimates, backdated using subsequent RR intervals in that
notification. They are not synchronized beat timestamps. Do not pass the auxiliary
preset's zero sampling rate to uniform-sampling DSP routines.

.. code-block:: python

from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets

params = BrainFlowInputParams()
params.serial_number = "YOUR_H10_DEVICE_ID"
board = BoardShim(BoardIds.POLAR_H10_BOARD, params)
board.prepare_session()
try:
board.start_stream()
# During recording:
ecg = board.get_board_data()
heart_rate = board.get_board_data(preset=BrainFlowPresets.AUXILIARY_PRESET)
board.stop_stream()
finally:
board.release_session()

A control error disconnects the transport to avoid accepting a delayed response
as a later command's acknowledgement. Release and reprepare to reconnect.
Validation uses Polar's published ECG fixture, HR/RR parser tests and a fake
transport exercising the real driver's handshake, acquisition, errors, timeouts
and restarts. BLE discovery, subscription timing and a physical H10 still need
validation on each supported operating system.

Reference: https://github.com/polarofficial/polar-ble-sdk
3 changes: 2 additions & 1 deletion java_package/brainflow/src/main/java/brainflow/BoardIds.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public enum BoardIds
IRONBCI_32_BOARD(65),
NEUROPAWN_KNIGHT_BOARD_IMU(66),
MUSE_S_ATHENA_BOARD(67),
SHIMMER3_BOARD(68);
SHIMMER3_BOARD(68),
POLAR_H10_BOARD(73);

private final int board_id;
private static final Map<Integer, BoardIds> bi_map = new HashMap<Integer, BoardIds> ();
Expand Down
1 change: 1 addition & 0 deletions julia_package/brainflow/src/board_shim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export BrainFlowInputParams
NEUROPAWN_KNIGHT_BOARD_IMU = 66
MUSE_S_ATHENA_BOARD = 67
SHIMMER3_BOARD = 68
POLAR_H10_BOARD = 73

end

Expand Down
1 change: 1 addition & 0 deletions matlab_package/brainflow/BoardIds.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@
NEUROPAWN_KNIGHT_BOARD_IMU(66)
MUSE_S_ATHENA_BOARD(67)
SHIMMER3_BOARD(68)
POLAR_H10_BOARD(73)
end
end
3 changes: 2 additions & 1 deletion nodejs_package/brainflow/brainflow.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export enum BoardIds {
IRONBCI_32_BOARD = 65,
NEUROPAWN_KNIGHT_BOARD_IMU = 66,
MUSE_S_ATHENA_BOARD = 67,
SHIMMER3_BOARD = 68
SHIMMER3_BOARD = 68,
POLAR_H10_BOARD = 73
}

export enum IpProtocolTypes {
Expand Down
1 change: 1 addition & 0 deletions python_package/brainflow/board_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class BoardIds(enum.IntEnum):
NEUROPAWN_KNIGHT_BOARD_IMU = 66 #:
MUSE_S_ATHENA_BOARD = 67 #:
SHIMMER3_BOARD = 68 #:
POLAR_H10_BOARD = 73 #:


class IpProtocolTypes(enum.IntEnum):
Expand Down
1 change: 1 addition & 0 deletions rust_package/brainflow/src/ffi/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub enum BoardIds {
NeuropawnKnightBoardImu = 66,
MuseSAthenaBoard = 67,
Shimmer3Board = 68,
PolarH10Board = 73,
}
#[repr(i32)]
#[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)]
Expand Down
33 changes: 33 additions & 0 deletions src/board_controller/ble_lib_board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,39 @@ simpleble_err_t BLELibBoard::simpleble_peripheral_notify (simpleble_peripheral_t
#endif
}

simpleble_err_t BLELibBoard::simpleble_peripheral_indicate (simpleble_peripheral_t handle,
simpleble_uuid_t service, simpleble_uuid_t characteristic,
void (*callback) (simpleble_peripheral_t handle, simpleble_uuid_t service,
simpleble_uuid_t characteristic, const uint8_t *data, size_t data_length, void *userdata),
void *userdata)
{
#ifndef STATIC_SIMPLEBLE
std::lock_guard<std::mutex> lock (BLELibBoard::mutex);
if (BLELibBoard::dll_loader == NULL)
{
safe_logger (spdlog::level::err, "BLELibBoard::dll_loader is not initialized");
return SIMPLEBLE_FAILURE;
}
simpleble_err_t (*func) (simpleble_peripheral_t, simpleble_uuid_t, simpleble_uuid_t,
void (*) (simpleble_peripheral_t, simpleble_uuid_t, simpleble_uuid_t, const uint8_t *,
size_t, void *),
void *) = (simpleble_err_t (*) (simpleble_peripheral_t, simpleble_uuid_t, simpleble_uuid_t,
void (*) (simpleble_peripheral_t, simpleble_uuid_t, simpleble_uuid_t, const uint8_t *,
size_t, void *),
void *))BLELibBoard::dll_loader->get_address ("simpleble_peripheral_indicate");
if (func == NULL)
{
safe_logger (
spdlog::level::err, "failed to get function address for simpleble_peripheral_indicate");
return SIMPLEBLE_FAILURE;
}

return func (handle, service, characteristic, callback, userdata);
#else
return ::simpleble_peripheral_indicate (handle, service, characteristic, callback, userdata);
#endif
}

simpleble_err_t BLELibBoard::simpleble_peripheral_unsubscribe (
simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic)
{
Expand Down
4 changes: 4 additions & 0 deletions src/board_controller/board_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include "ntl_wifi.h"
#include "pieeg_board.h"
#include "playback_file_board.h"
#include "polar_h10.h"
#include "shimmer3.h"
#include "streaming_board.h"
#include "synchroni_board.h"
Expand Down Expand Up @@ -315,6 +316,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params)
board = std::shared_ptr<Board> (
new KnightIMU ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD_IMU, params));
break;
case BoardIds::POLAR_H10_BOARD:
board = std::shared_ptr<Board> (new PolarH10 (params));
break;
case BoardIds::SHIMMER3_BOARD:
board = std::shared_ptr<Board> (new Shimmer3 (params));
break;
Expand Down
23 changes: 23 additions & 0 deletions src/board_controller/brainflow_boards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,29 @@ BrainFlowBoards::BrainFlowBoards()
{"battery_channel", 16},
{"other_channels", {17}}
};
brainflow_boards_json["boards"]["73"]["default"] =
{
{"name", "PolarH10"},
{"sampling_rate", 130},
{"package_num_channel", 0},
{"ecg_channels", {1}},
{"other_channels", {2}},
{"battery_channel", 3},
{"timestamp_channel", 4},
{"marker_channel", 5},
{"num_rows", 6}
};
brainflow_boards_json["boards"]["73"]["auxiliary"] =
{
{"name", "PolarH10HeartRate"},
{"sampling_rate", 0}, // Irregular HR/RR events, not a uniformly sampled waveform.
{"package_num_channel", 0},
{"other_channels", {1, 2, 3}},
{"timestamp_channel", 4},
{"marker_channel", 5},
{"num_rows", 6}
};

}

BrainFlowBoards boards_struct;
2 changes: 2 additions & 0 deletions src/board_controller/build.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ SET (BOARD_CONTROLLER_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_base.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_imu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/polar/polar_h10.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/shimmer3.cpp
)

Expand Down Expand Up @@ -157,6 +158,7 @@ target_include_directories (
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/polar/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/inc
)

Expand Down
6 changes: 6 additions & 0 deletions src/board_controller/inc/ble_lib_board.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class BLELibBoard : public Board
simpleble_uuid_t characteristic, const uint8_t *data, size_t data_length,
void *userdata),
void *userdata);
simpleble_err_t simpleble_peripheral_indicate (simpleble_peripheral_t handle,
simpleble_uuid_t service, simpleble_uuid_t characteristic,
void (*callback) (simpleble_peripheral_t handle, simpleble_uuid_t service,
simpleble_uuid_t characteristic, const uint8_t *data, size_t data_length,
void *userdata),
void *userdata);
simpleble_err_t simpleble_peripheral_unsubscribe (
simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic);
size_t simpleble_peripheral_manufacturer_data_count (simpleble_peripheral_t handle);
Expand Down
50 changes: 50 additions & 0 deletions src/board_controller/polar/inc/polar_h10.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#pragma once

#include <condition_variable>
#include <mutex>
#include <vector>

#include "ble_lib_board.h"

class PolarH10 : public BLELibBoard
{
simpleble_adapter_t adapter;
simpleble_peripheral_t peripheral;
std::vector<simpleble_peripheral_t> discovered;
bool control_subscribed;
bool data_subscribed;
bool hr_subscribed;
bool initialized;
bool streaming;
bool accept_data;
bool ready;
bool awaiting;
uint8_t operation;
int control_result;
std::vector<uint8_t> control_parameters;
uint32_t ecg_counter;
uint32_t hr_counter;
uint64_t last_timestamp;
double battery;
std::mutex mutex;
std::condition_variable received;
int transact (const std::vector<uint8_t> &command, std::vector<uint8_t> &response);

protected:
// Transport seam: protocol/lifecycle tests override only these BLE operations.
virtual int connect_device ();
virtual bool write_control (const std::vector<uint8_t> &command);
virtual void disconnect_device ();
void on_control (const uint8_t *data, size_t size);
void on_ecg (const uint8_t *data, size_t size);
void on_heart_rate (const uint8_t *data, size_t size);

public:
explicit PolarH10 (struct BrainFlowInputParams params);
~PolarH10 () override;
int prepare_session () override;
int start_stream (int buffer_size, const char *streamer_params) override;
int stop_stream () override;
int release_session () override;
int config_board (std::string config, std::string &response) override;
};
102 changes: 102 additions & 0 deletions src/board_controller/polar/inc/polar_protocol.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#pragma once

#include <stddef.h>

#include <cstdint>
#include <limits>
#include <vector>

namespace polar
{
inline uint16_t u16 (const uint8_t *data)
{
return uint16_t (data[0]) | (uint16_t (data[1]) << 8);
}

struct Ecg
{
uint64_t timestamp;
std::vector<int32_t> samples;
};

inline bool ecg (const uint8_t *data, size_t size, Ecg &result)
{
if ((size < 13) || (data[0] != 0) || (data[9] != 0) || ((size - 10) % 3 != 0))
return false;
Ecg pending;
pending.timestamp = 0;
for (size_t i = 0; i < 8; ++i)
pending.timestamp |= uint64_t (data[i + 1]) << (8 * i);
for (size_t i = 10; i < size; i += 3)
{
int32_t value =
int32_t (data[i]) | (int32_t (data[i + 1]) << 8) | (int32_t (data[i + 2]) << 16);
if (value & 0x800000)
value -= 0x1000000;
pending.samples.push_back (value); // Already microvolts, no ADC gain conversion.
}
result = pending;
return true;
}

inline bool supports_ecg_profile (const std::vector<uint8_t> &data)
{
bool rate = false, resolution = false;
size_t offset = 0;
// PMD setting widths, excluding security and unknown/new setting kinds.
const size_t widths[] = {2, 2, 2, 4, 1, 4};
while (offset < data.size ())
{
if (data.size () - offset < 2)
return false;
uint8_t type = data[offset++];
size_t count = data[offset++];
if ((type >= 6) || (count > (data.size () - offset) / widths[type]))
return false;
for (size_t i = 0; i < count; ++i, offset += widths[type])
{
if ((type == 0) && (u16 (data.data () + offset) == 130))
rate = true;
if ((type == 1) && (u16 (data.data () + offset) == 14))
resolution = true;
}
}
return rate && resolution;
}

struct HeartRate
{
double bpm;
double contact;
std::vector<double> rr_ms;
};

inline bool heart_rate (const uint8_t *data, size_t size, HeartRate &result)
{
if (size < 2)
return false;
uint8_t flags = data[0];
size_t offset = (flags & 1) ? 3 : 2;
if (size < offset)
return false;
HeartRate pending;
pending.bpm = (flags & 1) ? u16 (data + 1) : data[1];
pending.contact =
(flags & 4) ? ((flags & 2) ? 1.0 : 0.0) : std::numeric_limits<double>::quiet_NaN ();
if (flags & 8)
offset += 2; // Energy expended is not exposed by this profile.
if (offset > size)
return false;
if (flags & 16)
{
if ((size == offset) || ((size - offset) % 2 != 0))
return false;
for (; offset < size; offset += 2)
pending.rr_ms.push_back (u16 (data + offset) * (1000.0 / 1024.0));
}
if (offset != size)
return false;
result = pending;
return true;
}
}
Loading
Loading