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.
- protocentral_easyecg-0.2.0/.gitignore +6 -0
- protocentral_easyecg-0.2.0/CHANGELOG.md +34 -0
- protocentral_easyecg-0.2.0/LICENSE.md +21 -0
- protocentral_easyecg-0.2.0/PKG-INFO +122 -0
- protocentral_easyecg-0.2.0/README.md +108 -0
- protocentral_easyecg-0.2.0/examples/easyecg_diagnostics.py +148 -0
- protocentral_easyecg-0.2.0/examples/easyecg_heartrate.py +74 -0
- protocentral_easyecg-0.2.0/examples/easyecg_openview.py +108 -0
- protocentral_easyecg-0.2.0/examples/easyecg_plotter.py +44 -0
- protocentral_easyecg-0.2.0/examples/easyecg_raspberrypi.py +31 -0
- protocentral_easyecg-0.2.0/examples/easyecg_simpletest.py +44 -0
- protocentral_easyecg-0.2.0/protocentral_easyecg.py +633 -0
- protocentral_easyecg-0.2.0/pyproject.toml +31 -0
- protocentral_easyecg-0.2.0/tests/test_easyecg.py +501 -0
|
@@ -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
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml)
|
|
18
|
+
[](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
|
+

|
|
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
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyecg/actions/workflows/test.yml)
|
|
4
|
+
[](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
|
+

|
|
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))
|