Bluetooth (BLE) Heart Rate Display using UniHiker and Magene H64

What you’ll need:

 

DFRobot UniHiker

Magene H64 Heart Rate Monitor (BLE)

USB-C Cable

Basic Python knowledge

 

 

Overview:

Scan for BLE devices using bluepy

Connect to the Magene H64 heart rate monitor

Read and display heart rate on the UniHiker screen

HARDWARE LIST
1 DFRobot Unihiker M10
1 Magene Mover H64 Heart Rate Sensor
STEP 1
Using Jupyter, start a new terminal and install required Python package. This will be used to scan for BLE devices
CODE
pip install bluepy
STEP 2
Using Jupyter, start a new Journel, and use the script below to scan for BLE devices
CODE
from bluepy.btle import Scanner

scanner = Scanner()
print("Scanning for BLE devices...\n")
devices = scanner.scan(5.0)

for i, dev in enumerate(devices):
    print(f"Device {i+1}")
    print(f"  MAC Address : {dev.addr}")
    print(f"  RSSI        : {dev.rssi} dB")
    
    scan_name = None
    for (adtype, desc, value) in dev.getScanData():
        print(f"  {desc:25}: {value}")
        if desc == "Complete Local Name":
            scan_name = value

    if scan_name:
        print(f"  🟢 Likely Name: {scan_name}")
    print("-" * 50)

Run the script and look for your heart rate monitor Magene H64

Note down the MAC Address (e.g., C4:8A:7D:6B:2F:1A)

 

Luckily its easy to find, as the scan name matches the printed id on the back of the Magene H64

 

 

 

STEP 3
Use your BLE mac address, edit the script and read heart rate on the Unihiker
CODE
from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM
from unihiker import GUI
import time

# === User Configuration ===
AGE = 30
MAX_HEART_RATE = 220 - AGE
MAC_ADDRESS = "c7:9f:95:fe:32:cc"  # replace it your BLE sensor address

# === UniHiker GUI Setup ===
gui = GUI()
gui.fill_rect(x=0, y=0, w=240, h=320, color="white")
gui.draw_text(text="Heart Rate Monitor", x=10, y=5, font_size=14, color="black")

bpm_text = gui.draw_text(text="-- bpm", x=10, y=25, font_size=24, color="red")
zone_text = gui.draw_text(text="Zone: --", x=10, y=60, font_size=18, color="blue")
timer_text = gui.draw_text(text="Time: 00:00", x=10, y=85, font_size=16, color="purple")

# === Graph Settings ===
GRAPH_X_START = 10
GRAPH_Y_START = 120
GRAPH_WIDTH = 220
GRAPH_HEIGHT = 100
BAR_WIDTH = 3
BAR_SPACING = 1
MAX_BARS = 60

zone_samples = []
bar_history = []

last_zone_record_time = time.time()
last_minute_time = time.time()
session_start_time = None

retry_button = None

zone_colors = {
    "Below": "gray",
    "Zone 1 (Warm-up)": "lightblue",
    "Zone 2 (Fat burn)": "green",
    "Zone 3 (Endurance)": "yellow",
    "Zone 4 (Hardcore)": "orange",
    "Zone 5 (Max Effort)": "red",
}

# === Draw Legend (once) ===
def draw_legend():
    legend_x = 10
    legend_y = GRAPH_Y_START + GRAPH_HEIGHT + 10
    spacing_x = 45

    for idx, (zone_name, color) in enumerate([
        ("Z1", "lightblue"),
        ("Z2", "green"),
        ("Z3", "yellow"),
        ("Z4", "orange"),
        ("Z5", "red"),
    ]):
        box_x = legend_x + idx * spacing_x
        gui.fill_rect(x=box_x, y=legend_y, w=10, h=10, color=color)
        gui.draw_text(text=zone_name, x=box_x + 12, y=legend_y - 2, font_size=10, color="black")

draw_legend()

# === BLE Heart Rate Delegate ===
class HRDelegate(DefaultDelegate):
    def __init__(self):
        super().__init__()
        self.current_zone = "Below"

    def get_zone(self, bpm):
        percentage = (bpm / MAX_HEART_RATE) * 100
        if percentage < 50:
            return "Below"
        elif 50 <= percentage < 60:
            return "Zone 1 (Warm-up)"
        elif 60 <= percentage < 70:
            return "Zone 2 (Fat burn)"
        elif 70 <= percentage < 80:
            return "Zone 3 (Endurance)"
        elif 80 <= percentage < 90:
            return "Zone 4 (Hardcore)"
        else:
            return "Zone 5 (Max Effort)"

    def draw_bars(self):
        gui.fill_rect(x=GRAPH_X_START, y=GRAPH_Y_START, w=GRAPH_WIDTH, h=GRAPH_HEIGHT, color="white")
        gui.draw_line(x0=GRAPH_X_START, y0=GRAPH_Y_START, x1=GRAPH_X_START, y1=GRAPH_Y_START + GRAPH_HEIGHT, color="black")
        gui.draw_line(x0=GRAPH_X_START, y0=GRAPH_Y_START + GRAPH_HEIGHT, x1=GRAPH_X_START + GRAPH_WIDTH, y1=GRAPH_Y_START + GRAPH_HEIGHT, color="black")

        for idx, summary in enumerate(bar_history[-MAX_BARS:]):
            x = GRAPH_X_START + idx * (BAR_WIDTH + BAR_SPACING)
            y_bottom = GRAPH_Y_START + GRAPH_HEIGHT

            total_seconds = sum(summary.values())
            if total_seconds == 0:
                continue

            for zone, seconds in summary.items():
                if seconds == 0:
                    continue
                zone_height = int((seconds / 60) * GRAPH_HEIGHT)
                color = zone_colors.get(zone, "gray")
                gui.fill_rect(x=x, y=y_bottom - zone_height, w=BAR_WIDTH, h=zone_height, color=color)
                y_bottom -= zone_height

    def handleNotification(self, cHandle, data):
        global last_zone_record_time, last_minute_time, session_start_time

        flags = data[0]
        bpm = data[1] if flags & 0x01 == 0 else int.from_bytes(data[1:3], byteorder='little')
        print(f"BPM: {bpm}")

        bpm_text.config(text=f"{bpm} bpm")

        zone = self.get_zone(bpm)
        zone_text.config(text=f"{zone}")
        self.current_zone = zone

        if session_start_time is None:
            session_start_time = time.time()

        elapsed = int(time.time() - session_start_time)
        minutes = elapsed // 60
        seconds = elapsed % 60
        timer_text.config(text=f"Time: {minutes:02}:{seconds:02}")

        now = time.time()
        if now - last_zone_record_time >= 1.0:
            zone_samples.append(self.current_zone)
            last_zone_record_time = now

        if now - last_minute_time >= 60.0:
            print("Building minute bar...")
            summary = {z: 0 for z in zone_colors.keys()}
            for z in zone_samples:
                summary[z] += 1

            bar_history.append(summary)
            if len(bar_history) > MAX_BARS:
                bar_history.pop(0)

            self.draw_bars()
            zone_samples.clear()
            last_minute_time = now

# === Retry Button Utilities ===
def show_retry_button():
    global retry_button
    retry_button = gui.add_button(text="Retry", x=190, y=10, w=40, h=30, onclick=retry_handler)

def retry_handler():
    global retry_button
    if retry_button:
        gui.remove(retry_button)
        retry_button = None

    # Clear screen and show Connecting message immediately
    gui.fill_rect(x=0, y=0, w=240, h=320, color="white")
    gui.draw_text(text="Connecting...", x=30, y=140, font_size=20, color="black")
    gui.update()
    time.sleep(0.2)

    connect_device()

# === Connection Logic ===
def connect_device():
    global retry_button
    try:
        print("🔗 Connecting...")
        dev = Peripheral(MAC_ADDRESS, addrType=ADDR_TYPE_RANDOM)
        dev.setDelegate(HRDelegate())
        print("✅ Connected")

        dev.writeCharacteristic(0x000e, b"\x01\x00", withResponse=True)

        print("📡 Streaming heart rate...")
        while True:
            try:
                if dev.waitForNotifications(5.0):
                    continue
                print("⏳ Waiting for heart rate...")
            except Exception as e:
                print(f"❌ Lost connection mid-session: {e}")
                gui.fill_rect(x=0, y=0, w=240, h=320, color="white")
                gui.draw_text(text="Connection Lost", x=30, y=100, font_size=20, color="red")
                gui.draw_text(text="Tap Retry", x=30, y=140, font_size=14, color="black")
                show_retry_button()
                break

    except Exception as e:
        print(f"❌ Connection failed: {e}")
        gui.fill_rect(x=0, y=0, w=240, h=320, color="white")
        gui.draw_text(text="Connection Failed", x=30, y=100, font_size=20, color="red")
        gui.draw_text(text="Tap Retry", x=30, y=140, font_size=14, color="black")
        show_retry_button()

# === Start initial connection ===
connect_device()

What the code does:

 

This is a complete UniHiker project that connects to a Bluetooth Heart Rate Monitor (Magene H64) and displays:

Live Heart Rate (BPM)

Heart Rate Zone classification (e.g., Fat Burn, Endurance)

Session Timer showing elapsed minutes and seconds

Graphical Summary showing zone time distribution over the session (1-minute bars)

Color-coded Zone Legend for quick interpretation

Retry Button if connection fails or is lost

 

It uses:

bluepy to manage the BLE connection and receive heart rate notifications

UniHiker GUI to render real time data and interface elements on the screen

If the connection drops or fails, it shows a Retry button to let the user reconnect easily

 

License
All Rights
Reserved
licensBg
0