Files
drunkendotfiles/vendor/breakout-garden/examples/heartbeat/heartbeat.py
dissimulo 030172f523 Initial backup of LTP-305G matrix clock setup on matrixpi
Captures everything needed to redeploy the two-display clock (hour on I2C
0x61, minute on I2C 0x63) on a fresh Pi:

- Both systemd units (matrix0x61.service, matrix0x63.service)
- Deployed Pimoroni script tree, including the local %I (12-hour) clock
  customization
- Vendored upstream sources (ltp305-python, breakout-garden) so restore is
  fully offline-capable
- Boot config snippet enabling I2C
- install.sh that wires it all back up idempotently
- Inventory doc cross-referencing every live-system path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:32:39 -07:00

134 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# NOTE! This code should not be used for medical diagnosis. It's
# for fun/novelty use only, so bear that in mind while using it.
import sys
import time
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from threading import Thread
from luma.core.interface.serial import spi
from luma.oled.device import sh1106
from max30105 import MAX30105, HeartRate
print("""
NOTE! This code should not be used for medical diagnosis. It's
for fun/novelty use only, so bear that in mind while using it.
This Pimoroni Breakout Garden example requires a
MAX30105 Breakout and a 1.12" OLED Breakout (SPI).
The Pulse-O-Matic 6000 is a heartbeat plotter and BPM display.
Press Ctrl+C a couple times to exit.
""")
# Set up OLED
oled = sh1106(spi(port=0, device=1, gpio_DC=9), rotate=2, height=128, width=128)
# Load fonts
lsb_18 = ImageFont.truetype("fonts/LiberationSans-Bold.ttf", 18)
lsr_12 = ImageFont.truetype("fonts/LiberationSans-Regular.ttf", 12)
# Set up MAX30105 Breakout
max30105 = MAX30105()
max30105.setup(leds_enable=2)
max30105.set_led_pulse_amplitude(1, 0.2)
max30105.set_led_pulse_amplitude(2, 12.5)
max30105.set_led_pulse_amplitude(3, 0)
max30105.set_slot_mode(1, 'red')
max30105.set_slot_mode(2, 'ir')
max30105.set_slot_mode(3, 'off')
max30105.set_slot_mode(4, 'off')
hr = HeartRate(max30105)
data = []
running = True
bpm = 0
bpm_avg = 0
beat_detected = False
beat_status = False
def sample():
"""Function to thread heartbeat values separately to
OLED drawing"""
global bpm, bpm_avg, beat_detected, beat_status
average_over = 5
bpm_vals = [0 for x in range(average_over)]
last_beat = time.time()
while running:
t = time.time()
samples = max30105.get_samples()
if samples is not None:
for i in range(0, len(samples), 2):
ir = samples[i + 1]
beat_detected = hr.check_for_beat(ir)
if beat_detected:
beat_status = True
delta = t - last_beat
last_beat = t
bpm = 60 / delta
bpm_vals = bpm_vals[1:] + [bpm]
bpm_avg = sum(bpm_vals) / average_over
d = hr.low_pass_fir(ir & 0xff)
data.append(d)
if len(data) > 128:
data.pop(0)
time.sleep(0.05)
# The thread to measure acclerometer values
t = Thread(target=sample)
t.start()
# The main loop that draws values to the OLED
while True:
try:
img = Image.open("images/heartbeat.png").convert(oled.mode)
draw = ImageDraw.Draw(img)
# Draw the heartbeat trace
vals = data
new_vals = [x / float((max(vals) - min(vals))) * 32 for x in vals]
for i in range(1, len(new_vals)):
draw.line([(i - 1, 80 - new_vals[i - 1]), (i, 80 - new_vals[i])],
fill="white")
# Draw the Pulse-O-Matic "branding"
draw.rectangle([(0, 0), (128, 20)], fill="black")
if bpm_avg > 40:
draw.text((0, 1), "BPM: {:.2f}".format(bpm_avg), fill="white",
font=lsb_18)
else:
draw.text((0, 1), "BPM: --.--", fill="white", font=lsb_18)
if beat_status:
draw.text((115, 1), u"\u2665", fill="white", font=lsb_18)
beat_status = False
draw.line([(0, 20), (128, 20)], fill="white")
draw.rectangle([(0, 108), (128, 128)], fill="black")
draw.text((0, 110), "Pulse-O-Matic 6000", fill="white", font=lsr_12)
draw.line([(0, 108), (128, 108)], fill="white")
# Display on the OLED
oled.display(img)
except ZeroDivisionError:
pass
except KeyboardInterrupt:
running = False
sys.exit(0)