Skip to main content

Berry Oximeter 🫀

Banner

PyPI version Python 3.10+ License: MIT

A simple Python library for real-time data collection from BerryMed pulse oximeters via Bluetooth LE. Get heart rate, SpO2, and perfusion index readings with just a few lines of code!

[!WARNING] This library is for educational and research purposes only. It is not intended for medical diagnosis or treatment. Always consult with a qualified healthcare provider for medical advice.

Features ✨

  • 🔌 Auto-discovery - Automatically finds and connects to your BerryMed device
  • 📊 Real-time streaming - Get live readings with customizable callbacks
  • 📈 Data collection - Collect readings over time for analysis
  • 💾 CSV logging - Built-in data logging to CSV files
  • 🎯 Signal filtering - Filter readings by signal quality
  • 🐍 Pythonic API - Simple, intuitive interface
  • 🧪 Type hints - Full type hint support for better IDE experience

Installation 📦

Install via pip:

pip install berry-oximeter

Or using uv:

uv add berry-oximeter

Quick Start 🚀

Basic Usage

from berry_oximeter import BerryOximeter

# Create instance and connect
oximeter = BerryOximeter()
oximeter.connect()

# Enable console output
oximeter.log_to_console(True)

# Let it run for 30 seconds
import time

time.sleep(30)

# Disconnect
oximeter.disconnect()

Callback-based Streaming

def handle_reading(reading):
    if reading.is_valid:
        print(f"♥ SpO2: {reading.spo2}%, Pulse: {reading.pulse_rate} BPM")
    else:
        print(f"Status: {reading.status}")


oximeter = BerryOximeter()
oximeter.connect()
oximeter.start_streaming(handle_reading)

# Stream for 60 seconds
time.sleep(60)

oximeter.stop_streaming()
oximeter.disconnect()

Data Collection & Analysis

# Collect 60 seconds of data
oximeter = BerryOximeter()
oximeter.connect()

readings = oximeter.get_readings(duration_seconds=60)

# Analyze the data
valid_readings = [r for r in readings if r.is_valid]
avg_spo2 = sum(r.spo2 for r in valid_readings) / len(valid_readings)
avg_pulse = sum(r.pulse_rate for r in valid_readings) / len(valid_readings)

print(f"Average SpO2: {avg_spo2:.1f}%")
print(f"Average Pulse: {avg_pulse:.1f} BPM")

oximeter.disconnect()

CSV Logging

oximeter = BerryOximeter()
oximeter.connect()

# Start logging (auto-generates filename with timestamp)
filename = oximeter.start_logging()
print(f"Logging to: {filename}")

# Or specify your own filename
# oximeter.start_logging("patient_123.csv")

# Collect data
oximeter.log_to_console(True)
time.sleep(300)  # 5 minutes

# Stop logging
oximeter.stop_logging()
oximeter.disconnect()

Context Manager (Auto-cleanup)

with BerryOximeter() as oximeter:
    oximeter.connect()
    oximeter.log_to_console(True)
    time.sleep(30)
    # Automatically disconnects when done

API Reference 📚

BerryOximeter Class

The main interface for interacting with the pulse oximeter.

Connection Methods

  • connect(device_address=None, timeout=10.0) - Connect to device
  • disconnect() - Disconnect from device
  • is_connected - Property to check connection status

Data Access Methods

  • start_streaming(callback) - Start streaming with callback function
  • stop_streaming() - Stop streaming
  • get_reading(timeout=5.0) - Get a single reading
  • get_readings(duration_seconds) - Collect readings for specified duration

Logging Methods

  • start_logging(filename=None) - Start CSV logging
  • stop_logging() - Stop logging and return filename
  • log_to_console(enabled=True) - Enable/disable console output

Configuration

  • set_filter(min_signal_strength=None) - Filter by signal quality (0-8)

OximeterReading Object

Each reading contains:

  • timestamp - When the reading was taken
  • spo2 - Oxygen saturation (%) or None
  • pulse_rate - Heart rate (BPM) or None
  • pleth - Plethysmograph value or None
  • signal_strength - Signal quality (0-8)
  • is_valid - True if SpO2 and pulse are valid
  • status - Human-readable status ("reading", "no_finger", "searching", etc.)

Exceptions

  • DeviceNotFoundError - No BerryMed device found
  • ConnectionError - Failed to connect to device
  • NoDataError - No data received within timeout

Advanced Usage 🔧

Filtering Low-Quality Signals

# Only accept readings with signal strength >= 5
oximeter.set_filter(min_signal_strength=5)


def quality_callback(reading):
    print(f"High quality: SpO2 {reading.spo2}%, Signal: {reading.signal_strength}/8")


oximeter.start_streaming(quality_callback)

Custom Logging

import csv


def custom_logger(reading):
    if reading.is_valid:
        with open('custom_log.csv', 'a', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([
                reading.timestamp,
                reading.spo2,
                reading.pulse_rate,
                reading.pleth,
                reading.signal_strength
            ])


oximeter.start_streaming(custom_logger)

Async Integration

Since the library runs BLE operations in a separate thread, it works well with async code:

import asyncio


async def monitor_patient(patient_id, duration):
    oximeter = BerryOximeter()
    oximeter.connect()
    oximeter.start_logging(f"patient_{patient_id}.csv")

    await asyncio.sleep(duration)

    oximeter.stop_logging()
    oximeter.disconnect()


# Run multiple monitors concurrently
async def main():
    await asyncio.gather(
        monitor_patient("001", 300),
        monitor_patient("002", 300)
    )

Troubleshooting 🔍

Device Not Found

  • Make sure your BerryMed oximeter is turned on
  • Check that Bluetooth is enabled on your computer
  • The device name should be "BerryMed" - if yours is different, you'll need to modify the code
  • On Linux, you may need to run with sudo or add your user to the bluetooth group

Connection Issues

  • Ensure the device isn't already connected to another application
  • Try moving closer to the device
  • Some devices may need to be in pairing mode

No Readings

  • Make sure your finger is properly inserted
  • Keep your hand still - movement affects readings
  • Warm up cold fingers first
  • Check the signal strength indicator

Supported Devices 📱

This library is tested with:

  • BerryMed BM1000C
  • Other BerryMed pulse oximeters using the BCI protocol

The library should work with any Berry device that:

  • Advertises as "BerryMed" over Bluetooth LE
  • Uses the standard BCI protocol (5-byte packets)

Contributing 🤝

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Acknowledgments 🙏

  • Thanks to the Bleak project for the excellent BLE library
  • Inspired by the need for simple, reliable pulse oximeter data collection in research settings

Qatar University Machine Learning Group

Metadata

Release files for berry-oximeter 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for berry-oximeter 0.0.3
File Size Uploaded
berry_oximeter-0.0.3.tar.gz 886.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for berry-oximeter 0.0.3
File Interpreter ABI Platform
berry_oximeter-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 897.6 kB

Release files / berry_oximeter-0.0.3.tar.gz

Download URL berry_oximeter-0.0.3.tar.gz
Size 886.7 kB
Tags Source
SHA-256 checksum
How to use checksums
edd1a43e712ae4593c9847ae7dbd73d0bbb02a4ccd4e6c3ad6f0ae10b5288aa3
BLAKE2b-256 checksum
How to use checksums
d3208abb1c04c183302e076d3f3d366470732ae94b7c27116ed12283ecb1ed04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 4, 2025.

Transparency log

Release files / berry_oximeter-0.0.3-py3-none-any.whl

Download URL berry_oximeter-0.0.3-py3-none-any.whl
Size 10.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6dd39e30192ecddcd91d90f3716f39c77b058bf99058fc6e85c1d200410d3d72
BLAKE2b-256 checksum
How to use checksums
fc00023460c08067cf4d7e2cec652c420dab1f8c30dd896a8e9926874af0d2fd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 4, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.3 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page