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
pip install bluepy
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



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

