protocentral-easyecg 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .pytest_cache/
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this driver are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.2.0] - 2026-11-07
8
+
9
+ First public release. A port of the
10
+ [Protocentral easyECG Arduino library](https://github.com/Protocentral/protocentral_easyecg_arduino)
11
+ 1.0.0 for MicroPython and Raspberry Pi.
12
+
13
+ ### Added
14
+ - The Arduino API in snake_case, with the same defaults and validation rules: paced
15
+ sampling with missed-sample counting, 50/60 Hz notch, 40 Hz low-pass, optional
16
+ high-pass, heart rate from a simplified Pan-Tompkins detector, saturation, mains
17
+ pickup and a provisional lead-off heuristic, readings in µV at the electrodes.
18
+ - Low-level ADC passthroughs: `set_range()`, `set_data_rate()`, `set_mode()`,
19
+ `set_mux()` and `read_single_shot()`.
20
+ - Python-only extras: `set_gain()` / `get_gain()` and `.adc`.
21
+ - An example for every Arduino example: `easyecg_simpletest.py` and
22
+ `easyecg_raspberrypi.py` (`01`), `easyecg_plotter.py` (`02`), `easyecg_heartrate.py`
23
+ (`03`), `easyecg_openview.py` (`04`, the same packets for OpenView's easyECG Breakout
24
+ profile) and `easyecg_diagnostics.py` (`05`).
25
+ - Mocked-bus tests and an `mpy-cross` compile in CI, on every push and before every
26
+ PyPI publish.
27
+
28
+ ### Differences from the Arduino library
29
+ - The default ADC rate is 250 SPS rather than 1000, because interpreted Python can't
30
+ hold 1 kSPS through the filters. Output rates (125 / 250 Hz) are the same.
31
+ - `begin()` returns `False` on a missing board instead of writing to it.
32
+
33
+ Validated with the mocked-bus tests. Bench results on MicroPython and Raspberry Pi
34
+ hosts will be added to the README as they're measured.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.5
2
+ Name: protocentral-easyecg
3
+ Version: 0.2.0
4
+ Summary: MicroPython + Raspberry Pi driver for the Protocentral easyECG single-lead ECG breakout (TLA2024-based)
5
+ Project-URL: Homepage, https://protocentral.com
6
+ Project-URL: Source, https://github.com/Protocentral/protocentral-micropython-easyecg
7
+ Author-email: Protocentral Electronics <info@protocentral.com>
8
+ License: MIT
9
+ License-File: LICENSE.md
10
+ Keywords: easyecg,ecg,i2c,micropython,protocentral,qwiic,raspberry-pi,tla2024
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: protocentral-tla20xx>=1.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Protocentral easyECG MicroPython Library
16
+
17
+ [![Tests](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml/badge.svg)](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml)
18
+ [![Publish to PyPI](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/publish.yml/badge.svg)](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/publish.yml)
19
+
20
+ MicroPython library for the **Protocentral easyECG Breakout**: single-lead (Lead I) ECG over I²C / Qwiic. It handles the sampling, filtering and heart-rate detection. The same driver also runs on a Raspberry Pi.
21
+
22
+ ## Don't have one? [Buy it here](https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/)
23
+
24
+ ![Protocentral easyECG](https://raw.githubusercontent.com/Protocentral/protocentral-micropython-easyecg/main/assets/easyecg.jpg)
25
+
26
+ **Wiring, electrode placement and troubleshooting are in the [easyECG Getting Started guide](https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/docs/getting-started/).**
27
+
28
+ ## Links
29
+
30
+ | | |
31
+ |---|---|
32
+ | Product page | https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/ |
33
+ | Getting Started guide | https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/docs/getting-started/ |
34
+ | Arduino library | https://github.com/Protocentral/protocentral_easyecg_arduino |
35
+ | Hardware design files | https://github.com/Protocentral/protocentral_easyecg_hardware |
36
+
37
+ ## Features
38
+
39
+ - Paced sampling with missed-sample counting: 250 SPS in, 125 or 250 Hz out
40
+ - 50/60 Hz notch and 40 Hz low-pass in software; no high-pass by default, to keep the ST segment
41
+ - Heart rate from a simplified Pan-Tompkins R-peak detector
42
+ - Saturation, mains-pickup and a provisional lead-off flag
43
+ - Readings in µV at the electrodes
44
+ - A port of the [Arduino library](https://github.com/Protocentral/protocentral_easyecg_arduino) 1.0.0: the same API in snake_case, and a Python version of every Arduino example
45
+
46
+ > **Not a medical device. For research and education only.** The breakout has no patient isolation: when electrodes are on a person, power the host from a battery or connect through an [easyISO isolated Qwiic adapter](https://protocentral.com/product/easyiso-isolated-qwiic-adapter/). On a mains-powered host you get a flat line, and `is_saturated()` reports it.
47
+
48
+ ## Installation
49
+
50
+ **MicroPython** (Raspberry Pi Pico, ESP32, …):
51
+
52
+ ```bash
53
+ mpremote mip install github:Protocentral/protocentral-micropython-easyecg
54
+ ```
55
+
56
+ This also installs the [TLA20xx driver](https://github.com/Protocentral/protocentral-micropython-tla20xx). With Thonny, copy `protocentral_easyecg.py` and `protocentral_tla20xx.py` to the board instead.
57
+
58
+ **Raspberry Pi**, with I²C enabled (`sudo raspi-config nonint do_i2c 0`):
59
+
60
+ ```bash
61
+ python3 -m venv ~/easyecg && source ~/easyecg/bin/activate
62
+ pip install protocentral-easyecg
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ from machine import Pin, I2C
69
+ from protocentral_easyecg import EasyECG
70
+
71
+ i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000) # Pico: GP5, GP4. Change for your board
72
+ ecg = EasyECG(i2c) # default address 0x49
73
+ if not ecg.begin():
74
+ raise OSError("easyECG not found")
75
+
76
+ while True:
77
+ if ecg.update(): # True at the output rate, 125 Hz
78
+ print(ecg.ecg_microvolts(), ecg.heart_rate())
79
+ ```
80
+
81
+ On a Raspberry Pi, replace the I²C lines with `from protocentral_tla20xx_linux import I2C` and `EasyECG(I2C(bus=1))`.
82
+
83
+ Call `update()` as often as you can; it samples only when the clock is due, and never blocks. If `missed_samples()` grows, your loop is too slow.
84
+
85
+ ## API Reference
86
+
87
+ | Method | Description |
88
+ |---|---|
89
+ | `EasyECG(i2c, address=0x49)` / `begin()` | Construct / configure the ADC. `begin()` returns `False` if the board doesn't answer |
90
+ | `update()` | Non-blocking. `True` when a new output sample is ready |
91
+ | `ecg_microvolts()` / `ecg_filtered()` | Filtered output, in µV at the electrodes / in counts |
92
+ | `ecg_unfiltered_microvolts()` / `ecg_unfiltered()` | Plain ADC average |
93
+ | `heart_rate()` / `beat_detected()` / `last_rr_millis()` | bpm, 0 until established / true once per beat / last R-R interval |
94
+ | `is_saturated()` / `is_lead_off()` / `mains_microvolts()` | Signal-quality flags; lead-off is a provisional heuristic |
95
+ | `set_sampling(adc, out)` / `missed_samples()` | Sample clock (default 250, 125) / samples the loop couldn't take on time |
96
+ | `set_notch(hz)` / `set_low_pass(hz)` / `set_high_pass(hz)` | Filters: 50 Hz, 40 Hz, off by default. 0 disables |
97
+ | `calibrate_baseline(ms)` / `set_baseline(n)` | Measure and subtract the resting offset |
98
+
99
+ The full API, including the low-level ADC passthroughs, is documented in [protocentral_easyecg.py](https://github.com/Protocentral/protocentral-micropython-easyecg/blob/main/protocentral_easyecg.py).
100
+
101
+ ## Examples
102
+
103
+ | # | Example | Description |
104
+ |---|---|---|
105
+ | 01 | `easyecg_simpletest.py` / `easyecg_raspberrypi.py` | Filtered ECG in counts and µV (MicroPython / Raspberry Pi) |
106
+ | 02 | `easyecg_plotter.py` | Filtered and unfiltered traces for Thonny's plotter |
107
+ | 03 | `easyecg_heartrate.py` | Heart rate on every beat, with saturation and lead-off messages |
108
+ | 04 | `easyecg_openview.py` | Stream to [OpenView](https://github.com/Protocentral/protocentral_openview) 3.4+ (**easyECG Breakout**). Save it to the board as `main.py`, then close Thonny or `mpremote` before connecting OpenView |
109
+ | 05 | `easyecg_diagnostics.py` | I²C, ADC, noise, mains and sample-rate checks. **Run this first if something is wrong** |
110
+
111
+ The examples default to QT Py ESP32-C3 pins: change the `I2C(...)` line for your board.
112
+
113
+ ## License
114
+
115
+ - **Software:** MIT, see [LICENSE.md](https://github.com/Protocentral/protocentral-micropython-easyecg/blob/main/LICENSE.md)
116
+ - **Hardware:** CERN-OHL-P v2, in the [hardware repo](https://github.com/Protocentral/protocentral_easyecg_hardware)
117
+
118
+ Copyright (c) 2026 Protocentral Electronics.
119
+
120
+ ## Support
121
+
122
+ Open an issue on [GitHub](https://github.com/Protocentral/protocentral-micropython-easyecg/issues) or email support@protocentral.com.
@@ -0,0 +1,108 @@
1
+ # Protocentral easyECG MicroPython Library
2
+
3
+ [![Tests](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml/badge.svg)](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml)
4
+ [![Publish to PyPI](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/publish.yml/badge.svg)](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/publish.yml)
5
+
6
+ MicroPython library for the **Protocentral easyECG Breakout**: single-lead (Lead I) ECG over I²C / Qwiic. It handles the sampling, filtering and heart-rate detection. The same driver also runs on a Raspberry Pi.
7
+
8
+ ## Don't have one? [Buy it here](https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/)
9
+
10
+ ![Protocentral easyECG](https://raw.githubusercontent.com/Protocentral/protocentral-micropython-easyecg/main/assets/easyecg.jpg)
11
+
12
+ **Wiring, electrode placement and troubleshooting are in the [easyECG Getting Started guide](https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/docs/getting-started/).**
13
+
14
+ ## Links
15
+
16
+ | | |
17
+ |---|---|
18
+ | Product page | https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/ |
19
+ | Getting Started guide | https://protocentral.com/product/easyecg-single-lead-ecg-breakout-qwiic/docs/getting-started/ |
20
+ | Arduino library | https://github.com/Protocentral/protocentral_easyecg_arduino |
21
+ | Hardware design files | https://github.com/Protocentral/protocentral_easyecg_hardware |
22
+
23
+ ## Features
24
+
25
+ - Paced sampling with missed-sample counting: 250 SPS in, 125 or 250 Hz out
26
+ - 50/60 Hz notch and 40 Hz low-pass in software; no high-pass by default, to keep the ST segment
27
+ - Heart rate from a simplified Pan-Tompkins R-peak detector
28
+ - Saturation, mains-pickup and a provisional lead-off flag
29
+ - Readings in µV at the electrodes
30
+ - A port of the [Arduino library](https://github.com/Protocentral/protocentral_easyecg_arduino) 1.0.0: the same API in snake_case, and a Python version of every Arduino example
31
+
32
+ > **Not a medical device. For research and education only.** The breakout has no patient isolation: when electrodes are on a person, power the host from a battery or connect through an [easyISO isolated Qwiic adapter](https://protocentral.com/product/easyiso-isolated-qwiic-adapter/). On a mains-powered host you get a flat line, and `is_saturated()` reports it.
33
+
34
+ ## Installation
35
+
36
+ **MicroPython** (Raspberry Pi Pico, ESP32, …):
37
+
38
+ ```bash
39
+ mpremote mip install github:Protocentral/protocentral-micropython-easyecg
40
+ ```
41
+
42
+ This also installs the [TLA20xx driver](https://github.com/Protocentral/protocentral-micropython-tla20xx). With Thonny, copy `protocentral_easyecg.py` and `protocentral_tla20xx.py` to the board instead.
43
+
44
+ **Raspberry Pi**, with I²C enabled (`sudo raspi-config nonint do_i2c 0`):
45
+
46
+ ```bash
47
+ python3 -m venv ~/easyecg && source ~/easyecg/bin/activate
48
+ pip install protocentral-easyecg
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from machine import Pin, I2C
55
+ from protocentral_easyecg import EasyECG
56
+
57
+ i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000) # Pico: GP5, GP4. Change for your board
58
+ ecg = EasyECG(i2c) # default address 0x49
59
+ if not ecg.begin():
60
+ raise OSError("easyECG not found")
61
+
62
+ while True:
63
+ if ecg.update(): # True at the output rate, 125 Hz
64
+ print(ecg.ecg_microvolts(), ecg.heart_rate())
65
+ ```
66
+
67
+ On a Raspberry Pi, replace the I²C lines with `from protocentral_tla20xx_linux import I2C` and `EasyECG(I2C(bus=1))`.
68
+
69
+ Call `update()` as often as you can; it samples only when the clock is due, and never blocks. If `missed_samples()` grows, your loop is too slow.
70
+
71
+ ## API Reference
72
+
73
+ | Method | Description |
74
+ |---|---|
75
+ | `EasyECG(i2c, address=0x49)` / `begin()` | Construct / configure the ADC. `begin()` returns `False` if the board doesn't answer |
76
+ | `update()` | Non-blocking. `True` when a new output sample is ready |
77
+ | `ecg_microvolts()` / `ecg_filtered()` | Filtered output, in µV at the electrodes / in counts |
78
+ | `ecg_unfiltered_microvolts()` / `ecg_unfiltered()` | Plain ADC average |
79
+ | `heart_rate()` / `beat_detected()` / `last_rr_millis()` | bpm, 0 until established / true once per beat / last R-R interval |
80
+ | `is_saturated()` / `is_lead_off()` / `mains_microvolts()` | Signal-quality flags; lead-off is a provisional heuristic |
81
+ | `set_sampling(adc, out)` / `missed_samples()` | Sample clock (default 250, 125) / samples the loop couldn't take on time |
82
+ | `set_notch(hz)` / `set_low_pass(hz)` / `set_high_pass(hz)` | Filters: 50 Hz, 40 Hz, off by default. 0 disables |
83
+ | `calibrate_baseline(ms)` / `set_baseline(n)` | Measure and subtract the resting offset |
84
+
85
+ The full API, including the low-level ADC passthroughs, is documented in [protocentral_easyecg.py](https://github.com/Protocentral/protocentral-micropython-easyecg/blob/main/protocentral_easyecg.py).
86
+
87
+ ## Examples
88
+
89
+ | # | Example | Description |
90
+ |---|---|---|
91
+ | 01 | `easyecg_simpletest.py` / `easyecg_raspberrypi.py` | Filtered ECG in counts and µV (MicroPython / Raspberry Pi) |
92
+ | 02 | `easyecg_plotter.py` | Filtered and unfiltered traces for Thonny's plotter |
93
+ | 03 | `easyecg_heartrate.py` | Heart rate on every beat, with saturation and lead-off messages |
94
+ | 04 | `easyecg_openview.py` | Stream to [OpenView](https://github.com/Protocentral/protocentral_openview) 3.4+ (**easyECG Breakout**). Save it to the board as `main.py`, then close Thonny or `mpremote` before connecting OpenView |
95
+ | 05 | `easyecg_diagnostics.py` | I²C, ADC, noise, mains and sample-rate checks. **Run this first if something is wrong** |
96
+
97
+ The examples default to QT Py ESP32-C3 pins: change the `I2C(...)` line for your board.
98
+
99
+ ## License
100
+
101
+ - **Software:** MIT, see [LICENSE.md](https://github.com/Protocentral/protocentral-micropython-easyecg/blob/main/LICENSE.md)
102
+ - **Hardware:** CERN-OHL-P v2, in the [hardware repo](https://github.com/Protocentral/protocentral_easyecg_hardware)
103
+
104
+ Copyright (c) 2026 Protocentral Electronics.
105
+
106
+ ## Support
107
+
108
+ Open an issue on [GitHub](https://github.com/Protocentral/protocentral-micropython-easyecg/issues) or email support@protocentral.com.
@@ -0,0 +1,148 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
3
+ #
4
+ # easyecg_diagnostics.py - Arduino counterpart: 05.Diagnostics
5
+ #
6
+ # Run this when the trace looks wrong and you want numbers instead of guesses. Runs on
7
+ # MicroPython and on a Raspberry Pi.
8
+ #
9
+ # 1. Is the ADC on the bus, and at which address?
10
+ # 2. Did the configuration take? (CONFIG read back, expect 0x04C3)
11
+ # 3. Is the conversion register changing at all?
12
+ # 4. Every 2 s: noise and mains figures for the live signal.
13
+ #
14
+ # Step 4 prints, per 2 s window, in uV at the electrodes:
15
+ #
16
+ # filt_rms RMS of the filtered output
17
+ # raw_rms RMS of the unfiltered output
18
+ # mains mains pickup the notch is removing
19
+ # pp peak-to-peak of the filtered output (about the R-wave height with a heart)
20
+ # hr heart rate, bpm
21
+ # rate output samples per second actually delivered (should read 250; Python only)
22
+ # sat, lead amplifier saturated, lead-off heuristic
23
+ # missed samples the loop couldn't take on time (should stay 0)
24
+ #
25
+ # Two runs separate board noise from body noise:
26
+ #
27
+ # a) Short LA, RA and RL together at the jack (or clip all three leads to one
28
+ # electrode). filt_rms is now the board's own noise floor - expect a few uV.
29
+ # b) Electrodes on, sitting still. If filt_rms is far above (a) and mains is small, the
30
+ # extra noise is muscle or electrode contact. Filtering won't remove it.
31
+ #
32
+ # For research and education only - not a medical device. With electrodes on a person,
33
+ # power the host from a battery or connect through an easyISO isolated Qwiic adapter.
34
+
35
+ try:
36
+ from machine import Pin, I2C
37
+ # Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
38
+ i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
39
+ except ImportError: # Raspberry Pi
40
+ from protocentral_tla20xx_linux import I2C
41
+ i2c = I2C(bus=1)
42
+
43
+ import math
44
+
45
+ from protocentral_easyecg import (
46
+ EasyECG, EASYECG_I2C_ADDR_DEFAULT, EASYECG_I2C_ADDR_ALT, EASYECG_SATURATION_COUNTS,
47
+ EASYECG_DEFAULT_ADC_RATE, ticks_us, ticks_diff, sleep_ms,
48
+ )
49
+
50
+ MAINS_HZ = 50.0 # 50 or 60
51
+ EXPECTED_CONFIG = 0x04C3
52
+
53
+
54
+ def yes_no(flag):
55
+ return "Y" if flag else "-"
56
+
57
+
58
+ print()
59
+ print("=== Protocentral easyECG diagnostics ===")
60
+
61
+ print("--- 1. I2C scan")
62
+ found = None
63
+ for addr in i2c.scan():
64
+ label = ""
65
+ if addr in (EASYECG_I2C_ADDR_DEFAULT, EASYECG_I2C_ADDR_ALT):
66
+ label = " <- easyECG"
67
+ if found is None:
68
+ found = addr
69
+ print(" device at 0x{:02X}{}".format(addr, label))
70
+ if found is None:
71
+ raise OSError("easyECG not found at 0x49/0x48 - check wiring and power")
72
+
73
+ ecg = EasyECG(i2c, found)
74
+ ecg.begin()
75
+ ecg.set_sampling(EASYECG_DEFAULT_ADC_RATE, 250) # 250 Hz out, as in the Arduino sketch
76
+ ecg.set_notch(MAINS_HZ)
77
+
78
+ # Bit 15 reads 0 while a conversion is in progress, so it is masked off.
79
+ print("--- 2. CONFIG read-back")
80
+ conf = ecg.read_config()
81
+ print(" read 0x{:04X}, expected 0x{:04X} (+/-2.048 V, continuous, 3300 SPS) {}".format(
82
+ conf, EXPECTED_CONFIG, "OK" if (conf & 0x7FFF) == EXPECTED_CONFIG else "MISMATCH"))
83
+
84
+ print("--- 3. Conversion register activity")
85
+ lo = 32767
86
+ hi = -32768
87
+ total = 0
88
+ for _ in range(500):
89
+ v = ecg.read_raw_uncorrected()
90
+ lo = min(lo, v)
91
+ hi = max(hi, v)
92
+ total += v
93
+ sleep_ms(1)
94
+ print(" 500 samples: min {} max {} mean {}".format(lo, hi, int(total / 500)))
95
+ if lo == hi:
96
+ print(" STUCK: the ADC returns a constant. Check power and the CONFIG word.")
97
+ elif hi >= EASYECG_SATURATION_COUNTS or lo <= -EASYECG_SATURATION_COUNTS:
98
+ print(" Near a rail: electrode off, or mains-powered host without isolation.")
99
+ else:
100
+ print(" OK - converting.")
101
+
102
+ ecg.calibrate_baseline(1000)
103
+ print(" baseline {} counts".format(ecg.get_baseline()))
104
+
105
+ print("--- 4. Live figures, uV at the electrodes, every 2 s")
106
+ print(" ADC {} SPS, output {} Hz".format(ecg.adc_rate_hz(), ecg.output_rate_hz()))
107
+ print(" filt_rms raw_rms mains pp hr rate sat lead missed")
108
+
109
+ window = 2 * ecg.output_rate_hz()
110
+ skip = 1 # let the filters settle first
111
+ sum_f = sum_f2 = sum_r = sum_r2 = 0.0
112
+ min_f = 1e9
113
+ max_f = -1e9
114
+ n = 0
115
+ t0 = ticks_us()
116
+
117
+ while True:
118
+ if not ecg.update():
119
+ continue
120
+
121
+ f = ecg.ecg_microvolts()
122
+ r = ecg.ecg_unfiltered_microvolts()
123
+ sum_f += f
124
+ sum_f2 += f * f
125
+ sum_r += r
126
+ sum_r2 += r * r
127
+ min_f = min(min_f, f)
128
+ max_f = max(max_f, f)
129
+ n += 1
130
+ if n < window:
131
+ continue
132
+
133
+ t1 = ticks_us()
134
+ if skip:
135
+ skip -= 1
136
+ else:
137
+ rms_f = math.sqrt(max(0.0, sum_f2 / n - (sum_f / n) ** 2))
138
+ rms_r = math.sqrt(max(0.0, sum_r2 / n - (sum_r / n) ** 2))
139
+ rate = n * 1000000.0 / ticks_diff(t1, t0)
140
+ print(" {:8.1f} {:8.1f} {:8.1f} {:8.1f} {:7.1f} {:7.1f} {} {} {}".format(
141
+ rms_f, rms_r, ecg.mains_microvolts(), max_f - min_f, ecg.heart_rate(), rate,
142
+ yes_no(ecg.is_saturated()), yes_no(ecg.is_lead_off()), ecg.missed_samples()))
143
+
144
+ sum_f = sum_f2 = sum_r = sum_r2 = 0.0
145
+ min_f = 1e9
146
+ max_f = -1e9
147
+ n = 0
148
+ t0 = ticks_us()
@@ -0,0 +1,74 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
3
+ #
4
+ # easyecg_heartrate.py - Arduino counterpart: 03.HeartRate
5
+ #
6
+ # Prints the heart rate on every detected beat, blinks an LED if the board has one, and
7
+ # says so plainly when the signal can't be trusted. Runs on MicroPython and on a Raspberry
8
+ # Pi (no LED there).
9
+ #
10
+ # The detector is a simplified Pan-Tompkins: it needs about 2 s to learn the signal level,
11
+ # then averages the last 8 R-R intervals. Expect a number after 3-5 beats.
12
+ #
13
+ # Status messages, once a second:
14
+ # SATURATED - the amplifier is pinned at a rail. Usually a mains-powered host (switch
15
+ # to battery, or use an easyISO), or an electrode that has come off.
16
+ # LEAD OFF? - mains pickup is far above normal, which is what a loose or missing
17
+ # electrode looks like. Provisional heuristic - see the README.
18
+ #
19
+ # For research and education only - not a medical device. With electrodes on a person,
20
+ # power the host from a battery or connect through an easyISO isolated Qwiic adapter.
21
+
22
+ try:
23
+ from machine import Pin, I2C
24
+ # Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
25
+ i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
26
+ try:
27
+ led = Pin("LED", Pin.OUT) # Pico / Pico W; boards without a named LED skip it
28
+ except Exception: # noqa: BLE001 - ports raise different errors
29
+ led = None
30
+ except ImportError: # Raspberry Pi
31
+ from protocentral_tla20xx_linux import I2C
32
+ i2c = I2C(bus=1)
33
+ led = None
34
+
35
+ from protocentral_easyecg import EasyECG, ticks_us, ticks_diff
36
+
37
+ MAINS_HZ = 50.0 # 50 or 60
38
+
39
+ ecg = EasyECG(i2c) # ships at 0x49
40
+ if not ecg.begin():
41
+ raise OSError("easyECG not found - check wiring and I2C address (0x49)")
42
+
43
+ ecg.set_notch(MAINS_HZ)
44
+ ecg.calibrate_baseline(1000)
45
+ print("Sit still - learning the signal for 2 s...")
46
+
47
+ led_on_at = None
48
+ last_status = ticks_us()
49
+
50
+ while True:
51
+ now = ticks_us()
52
+
53
+ if led_on_at is not None and ticks_diff(now, led_on_at) >= 60000:
54
+ led.value(0)
55
+ led_on_at = None
56
+
57
+ if not ecg.update():
58
+ continue
59
+
60
+ if ecg.beat_detected():
61
+ if led is not None:
62
+ led.value(1)
63
+ led_on_at = now
64
+ hr = ecg.heart_rate()
65
+ print("Beat RR {} ms HR {}".format(
66
+ ecg.last_rr_millis(), "{:.0f} bpm".format(hr) if hr > 0.0 else "--"))
67
+
68
+ # Signal-quality status once a second.
69
+ if ticks_diff(now, last_status) >= 1000000:
70
+ last_status = now
71
+ if ecg.is_saturated():
72
+ print("SATURATED - battery host or isolated adapter? electrode off?")
73
+ elif ecg.is_lead_off():
74
+ print("LEAD OFF? mains pickup {:.0f} uV RMS".format(ecg.mains_microvolts()))
@@ -0,0 +1,108 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
3
+ #
4
+ # easyecg_openview.py - Arduino counterpart: 04.OpenView
5
+ #
6
+ # Streams the ECG into the Protocentral OpenView desktop app over USB serial. MicroPython
7
+ # only (it needs sys.stdout.buffer).
8
+ #
9
+ # In OpenView 3.4 or later: Connect -> USB -> select "easyECG Breakout"
10
+ #
11
+ # On earlier versions the "ADS1292R Breakout" profile also reads this stream, with the
12
+ # labels wrong: its ECG is the filtered trace, its Respiration the unfiltered trace, its
13
+ # HR the heart rate and its RR the signal status.
14
+ #
15
+ # Same packets as the Arduino sketch, 125 per second, written as raw bytes to the USB
16
+ # serial port (the baud rate setting doesn't matter over USB CDC):
17
+ #
18
+ # 0x0A 0xFA 0x08 0x00 0x02 | 8-byte payload | 0x00 0x0B
19
+ # payload, int16 little-endian:
20
+ # ECG (filtered) <- notch + 40 Hz low-pass, counts
21
+ # ECG (unfiltered) <- plain average of the ADC samples (0 if SECOND_CHANNEL_UNFILTERED
22
+ # is False). Hidden by default in OpenView.
23
+ # Heart rate <- bpm, rounded
24
+ # Signal <- 0 = OK, 1 = lead-off / mains pickup, 2 = amplifier saturated
25
+ #
26
+ # The driver samples at 250 SPS (Arduino: 1000) and decimates to 125 Hz, so OpenView sees
27
+ # the same rate and scale. No digital high-pass by default: the board's 0.5 Hz analog
28
+ # corner is already there, and a second pole distorts the ST segment.
29
+ #
30
+ # HOW TO RUN: save this file to the board as main.py and reset it, then close Thonny or
31
+ # mpremote so OpenView can open the port. The REPL shares that port, so `mpremote run`
32
+ # can't be used here: it would read the packets itself. Don't print() anything else -
33
+ # OpenView expects binary packets only. To stop it, connect Thonny and press Ctrl-C.
34
+ #
35
+ # Needs a port whose sys.stdout has a .buffer (ESP32, RP2040/RP2350 and most others).
36
+ #
37
+ # Download OpenView: https://github.com/Protocentral/protocentral_openview
38
+ #
39
+ # On a mains-powered host the driven-right-leg amplifier saturates and there is no trace.
40
+ # For research and education only - not a medical device. With electrodes on a person,
41
+ # power the host from a battery or connect through an easyISO isolated Qwiic adapter.
42
+
43
+ import struct
44
+ import sys
45
+
46
+ from machine import Pin, I2C
47
+ from protocentral_easyecg import EasyECG, EASYECG_DEFAULT_ADC_RATE, sleep_ms
48
+
49
+ # ---- user settings ------------------------------------------------------------------
50
+ MAINS_HZ = 50.0 # 50 or 60
51
+ LOW_PASS_HZ = 40.0 # 0 disables the digital low-pass
52
+ HIGH_PASS_HZ = 0.0 # 0 = off (keeps ST shape); 0.5 for wander
53
+ SECOND_CHANNEL_UNFILTERED = True
54
+
55
+ # Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
56
+ i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
57
+
58
+ # ---- Protocentral OpenView packet format -------------------------------------------
59
+ CES_CMDIF_PKT_START_1 = 0x0A
60
+ CES_CMDIF_PKT_START_2 = 0xFA
61
+ CES_CMDIF_TYPE_DATA = 0x02
62
+ CES_CMDIF_PKT_STOP = 0x0B
63
+ DATA_LEN = 0x08
64
+
65
+ # One preallocated packet: header (5) + payload (8) + footer (2). Only the payload changes.
66
+ packet = bytearray(5 + DATA_LEN + 2)
67
+ packet[0] = CES_CMDIF_PKT_START_1
68
+ packet[1] = CES_CMDIF_PKT_START_2
69
+ packet[2] = DATA_LEN
70
+ packet[3] = 0
71
+ packet[4] = CES_CMDIF_TYPE_DATA
72
+ packet[13] = 0
73
+ packet[14] = CES_CMDIF_PKT_STOP
74
+
75
+ out = getattr(sys.stdout, "buffer", sys.stdout)
76
+
77
+
78
+ def send_data_packet(ecg_ch, second_ch, hr, status):
79
+ struct.pack_into("<hhhh", packet, 5, ecg_ch, second_ch, hr, status)
80
+ out.write(packet)
81
+
82
+
83
+ ecg = EasyECG(i2c) # ships at 0x49
84
+ if not ecg.begin():
85
+ # No text output - the host is expecting binary packets.
86
+ while True:
87
+ sleep_ms(500)
88
+
89
+ ecg.set_sampling(EASYECG_DEFAULT_ADC_RATE, 125)
90
+ ecg.set_notch(MAINS_HZ)
91
+ ecg.set_low_pass(LOW_PASS_HZ)
92
+ ecg.set_high_pass(HIGH_PASS_HZ)
93
+
94
+ # Zero the amplifier offset over one second. Sit still.
95
+ ecg.calibrate_baseline(1000)
96
+
97
+ while True:
98
+ if not ecg.update():
99
+ continue
100
+
101
+ status = 0
102
+ if ecg.is_saturated():
103
+ status = 2
104
+ elif ecg.is_lead_off():
105
+ status = 1
106
+
107
+ second = ecg.ecg_unfiltered() if SECOND_CHANNEL_UNFILTERED else 0
108
+ send_data_packet(ecg.ecg_filtered(), second, int(ecg.heart_rate() + 0.5), status)
@@ -0,0 +1,44 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
3
+ #
4
+ # easyecg_plotter.py - Arduino counterpart: 02.SerialPlotter
5
+ #
6
+ # The ECG for a plotter, such as Thonny's View -> Plotter. Runs on MicroPython and on a
7
+ # Raspberry Pi.
8
+ #
9
+ # Two traces, in uV at the electrodes:
10
+ # Filtered - 50 Hz notch + 40 Hz low-pass: what you normally want
11
+ # Unfiltered - the plain ADC average, offset below so the two don't overlap
12
+ #
13
+ # Output is 250 Hz, as in the Arduino sketch, so the QRS keeps its shape. If
14
+ # missed_samples() grows on a slow board, change OUTPUT_HZ to 125.
15
+ #
16
+ # For research and education only - not a medical device. With electrodes on a person,
17
+ # power the host from a battery or connect through an easyISO isolated Qwiic adapter.
18
+
19
+ try:
20
+ from machine import Pin, I2C
21
+ # Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
22
+ i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
23
+ except ImportError: # Raspberry Pi
24
+ from protocentral_tla20xx_linux import I2C
25
+ i2c = I2C(bus=1)
26
+
27
+ from protocentral_easyecg import EasyECG, EASYECG_DEFAULT_ADC_RATE
28
+
29
+ MAINS_HZ = 50.0 # 50 or 60
30
+ OUTPUT_HZ = 250
31
+ UNFILTERED_OFFSET = 1500.0 # uV, vertical spacing between the traces
32
+
33
+ ecg = EasyECG(i2c) # ships at 0x49
34
+ if not ecg.begin():
35
+ raise OSError("easyECG not found - check wiring and I2C address (0x49)")
36
+
37
+ ecg.set_sampling(EASYECG_DEFAULT_ADC_RATE, OUTPUT_HZ)
38
+ ecg.set_notch(MAINS_HZ)
39
+ ecg.calibrate_baseline(1000) # one second, sitting still
40
+
41
+ while True:
42
+ if ecg.update():
43
+ print("Filtered:{:.0f},Unfiltered:{:.0f}".format(
44
+ ecg.ecg_microvolts(), ecg.ecg_unfiltered_microvolts() - UNFILTERED_OFFSET))