protocentral-easyecg 0.2.0__py3-none-any.whl
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,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,5 @@
|
|
|
1
|
+
protocentral_easyecg.py,sha256=EGVcQ7QIMBwAb4uVwEFtg8DdluoEH9geDs9Lp2z0nmU,22435
|
|
2
|
+
protocentral_easyecg-0.2.0.dist-info/METADATA,sha256=aR-t07hWoAJhTpHcBiqr1b0mh4zA9fM3Ge66gjOorBU,6766
|
|
3
|
+
protocentral_easyecg-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
4
|
+
protocentral_easyecg-0.2.0.dist-info/licenses/LICENSE.md,sha256=cgO4OFpz7oAWflQfXcqCpMv8MXBqds5RfR-423fycQw,1100
|
|
5
|
+
protocentral_easyecg-0.2.0.dist-info/RECORD,,
|
|
@@ -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.
|
protocentral_easyecg.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
|
|
3
|
+
#
|
|
4
|
+
# MicroPython driver for the Protocentral easyECG single-lead ECG breakout. An INA333
|
|
5
|
+
# and two OPA2333 stages (total gain x535, roughly 0.5-31 Hz) feed an on-board TI
|
|
6
|
+
# TLA2024 12-bit ADC, read differentially against the buffered 1.65 V mid-rail. This
|
|
7
|
+
# library DEPENDS ON the TLA20xx driver and adds the easyECG configuration, the paced
|
|
8
|
+
# filter chain and the heart-rate detector.
|
|
9
|
+
#
|
|
10
|
+
# Ported from the Protocentral easyECG Arduino library v1.0.0 (single source of truth):
|
|
11
|
+
# https://github.com/Protocentral/protocentral_easyecg_arduino
|
|
12
|
+
# Same public surface, in snake_case: begin() ... read_single_shot(). Python-only extras
|
|
13
|
+
# are set_gain() / get_gain() and the .adc attribute.
|
|
14
|
+
#
|
|
15
|
+
# Same chain as the Arduino library:
|
|
16
|
+
#
|
|
17
|
+
# ADC -> 50/60 Hz notch -> 4th-order Butterworth low-pass (40 Hz)
|
|
18
|
+
# -> decimate to the output rate -> optional high-pass (OFF by default)
|
|
19
|
+
# -> R-peak detector -> heart rate
|
|
20
|
+
#
|
|
21
|
+
# One deliberate difference: the default ADC rate is 250 SPS, not 1000. Interpreted
|
|
22
|
+
# Python can't run these filters at 1 kSPS on most boards. The filters are built for an
|
|
23
|
+
# exact rate, so a clock the host can actually hold beats a faster one it can't. At
|
|
24
|
+
# 250 SPS the 50/60 Hz notch and the 40 Hz low-pass are still well inside Nyquist, and
|
|
25
|
+
# both output rates the Arduino examples use (125 and 250 Hz) are reachable.
|
|
26
|
+
#
|
|
27
|
+
# Requires: protocentral_tla20xx (mip/pip dependency)
|
|
28
|
+
|
|
29
|
+
import math
|
|
30
|
+
|
|
31
|
+
__version__ = "0.2.0"
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from time import ticks_us, ticks_diff, ticks_add, sleep_ms, sleep_us
|
|
35
|
+
except ImportError: # CPython (Raspberry Pi)
|
|
36
|
+
import time as _time
|
|
37
|
+
|
|
38
|
+
def ticks_us():
|
|
39
|
+
return int(_time.monotonic() * 1000000)
|
|
40
|
+
|
|
41
|
+
def ticks_diff(a, b):
|
|
42
|
+
return a - b
|
|
43
|
+
|
|
44
|
+
def ticks_add(a, b):
|
|
45
|
+
return a + b
|
|
46
|
+
|
|
47
|
+
def sleep_ms(ms):
|
|
48
|
+
_time.sleep(ms / 1000.0)
|
|
49
|
+
|
|
50
|
+
def sleep_us(us):
|
|
51
|
+
_time.sleep(us / 1000000.0)
|
|
52
|
+
|
|
53
|
+
from protocentral_tla20xx import (
|
|
54
|
+
TLA20XX, FSR_6_144V, FSR_4_096V, FSR_2_048V, FSR_1_024V, FSR_0_512V, FSR_0_256V,
|
|
55
|
+
DR_128SPS, DR_250SPS, DR_490SPS, DR_920SPS, DR_1600SPS, DR_2400SPS, DR_3300SPS,
|
|
56
|
+
MUX_AIN0_AIN1, MUX_AIN0_AIN3, MUX_AIN1_AIN3, MUX_AIN2_AIN3,
|
|
57
|
+
MUX_AIN0_GND, MUX_AIN1_GND, MUX_AIN2_GND, MUX_AIN3_GND,
|
|
58
|
+
OP_CONTINUOUS, OP_SINGLE, _REG_CONFIG,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Arduino's easyECG::Mode names for the TLA20xx OP_* values.
|
|
62
|
+
MODE_CONTINUOUS = OP_CONTINUOUS
|
|
63
|
+
MODE_SINGLE_SHOT = OP_SINGLE
|
|
64
|
+
|
|
65
|
+
# JP1 bridged 1-2 straps ADDR to 3.3 V -> 0x49. Cut and bridge 2-3 for 0x48. An easyEMG
|
|
66
|
+
# ships at 0x48, so the two share a bus without touching the jumper.
|
|
67
|
+
EASYECG_I2C_ADDR_DEFAULT = 0x49
|
|
68
|
+
EASYECG_I2C_ADDR_ALT = 0x48
|
|
69
|
+
|
|
70
|
+
# INA333 x11 (R10 = 10k) times 1 + R14/R19 = 1 + 100k/2.1k = x48.6. Design value; pass
|
|
71
|
+
# your measured gain to set_gain(). Only absolute microvolts depend on it.
|
|
72
|
+
EASYECG_DEFAULT_GAIN = 535.0
|
|
73
|
+
EASYECG_GAIN = EASYECG_DEFAULT_GAIN # Arduino name
|
|
74
|
+
|
|
75
|
+
EASYECG_HPF_HZ = 0.49 # C6 1 uF / R13 324k, 1-pole
|
|
76
|
+
EASYECG_LPF_HZ = 31.0 # Sallen-Key, 33k / 100n, Q = 0.5
|
|
77
|
+
|
|
78
|
+
# The OPA2333 swings to about +/-1.6 V around mid-rail; 1500 counts leaves some margin.
|
|
79
|
+
EASYECG_SATURATION_COUNTS = 1500
|
|
80
|
+
|
|
81
|
+
EASYECG_CONV_REG = 0x00
|
|
82
|
+
EASYECG_CONF_REG = 0x01
|
|
83
|
+
|
|
84
|
+
# Arduino defaults to 1000 SPS (500 on AVR). See the note at the top of this file.
|
|
85
|
+
EASYECG_DEFAULT_ADC_RATE = 250
|
|
86
|
+
EASYECG_DEFAULT_OUTPUT_RATE = 125
|
|
87
|
+
EASYECG_MAX_DECIMATION = 16
|
|
88
|
+
EASYECG_MWI_MAX = 40
|
|
89
|
+
EASYECG_RR_HISTORY = 8
|
|
90
|
+
|
|
91
|
+
# PROVISIONAL: not yet validated on the bench per electrode.
|
|
92
|
+
_DEFAULT_LEADOFF_UV = 200.0
|
|
93
|
+
|
|
94
|
+
_BW4_Q1 = 0.54119610
|
|
95
|
+
_BW4_Q2 = 1.30656296
|
|
96
|
+
|
|
97
|
+
# TLA2024 conversion time per DR code, microseconds (datasheet minimum rates).
|
|
98
|
+
_CONV_US = (7813, 4000, 2041, 1087, 625, 417, 303)
|
|
99
|
+
|
|
100
|
+
_FSR_VOLTS = {
|
|
101
|
+
FSR_6_144V: 6.144, FSR_4_096V: 4.096, FSR_2_048V: 2.048,
|
|
102
|
+
FSR_1_024V: 1.024, FSR_0_512V: 0.512, FSR_0_256V: 0.256,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _Biquad:
|
|
107
|
+
"""RBJ cookbook biquad, direct form II transposed."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, b0=1.0, b1=0.0, b2=0.0, a1=0.0, a2=0.0):
|
|
110
|
+
self.b0, self.b1, self.b2, self.a1, self.a2 = b0, b1, b2, a1, a2
|
|
111
|
+
self.z1 = 0.0
|
|
112
|
+
self.z2 = 0.0
|
|
113
|
+
|
|
114
|
+
def clear(self):
|
|
115
|
+
self.z1 = 0.0
|
|
116
|
+
self.z2 = 0.0
|
|
117
|
+
|
|
118
|
+
def run(self, x):
|
|
119
|
+
y = (self.b0 * x) + self.z1
|
|
120
|
+
self.z1 = (self.b1 * x) - (self.a1 * y) + self.z2
|
|
121
|
+
self.z2 = (self.b2 * x) - (self.a2 * y)
|
|
122
|
+
return y
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _low_pass(fc, fs, q):
|
|
126
|
+
w0 = 2.0 * math.pi * fc / fs
|
|
127
|
+
cosw0 = math.cos(w0)
|
|
128
|
+
alpha = math.sin(w0) / (2.0 * q)
|
|
129
|
+
a0 = 1.0 + alpha
|
|
130
|
+
b0 = ((1.0 - cosw0) * 0.5) / a0
|
|
131
|
+
return _Biquad(b0, (1.0 - cosw0) / a0, b0, (-2.0 * cosw0) / a0, (1.0 - alpha) / a0)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _notch(f0, fs, q):
|
|
135
|
+
w0 = 2.0 * math.pi * f0 / fs
|
|
136
|
+
cosw0 = math.cos(w0)
|
|
137
|
+
alpha = math.sin(w0) / (2.0 * q)
|
|
138
|
+
a0 = 1.0 + alpha
|
|
139
|
+
b0 = 1.0 / a0
|
|
140
|
+
b1 = (-2.0 * cosw0) / a0
|
|
141
|
+
return _Biquad(b0, b1, b0, b1, (1.0 - alpha) / a0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _clamp_counts(v):
|
|
145
|
+
# Arduino clampCounts(): saturate to int16, round half away from zero.
|
|
146
|
+
if v > 32767.0:
|
|
147
|
+
v = 32767.0
|
|
148
|
+
elif v < -32768.0:
|
|
149
|
+
v = -32768.0
|
|
150
|
+
return int(v + 0.5) if v >= 0.0 else int(v - 0.5)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class EasyECG:
|
|
154
|
+
"""Driver for the Protocentral easyECG breakout.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
from machine import Pin, I2C
|
|
158
|
+
from protocentral_easyecg import EasyECG
|
|
159
|
+
i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000) # QT Py C3: SCL=6, SDA=5
|
|
160
|
+
ecg = EasyECG(i2c)
|
|
161
|
+
ecg.begin()
|
|
162
|
+
while True:
|
|
163
|
+
if ecg.update():
|
|
164
|
+
print(ecg.ecg_microvolts(), ecg.heart_rate())
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, i2c, address=EASYECG_I2C_ADDR_DEFAULT):
|
|
168
|
+
self.i2c = i2c
|
|
169
|
+
self.address = address
|
|
170
|
+
self.adc = TLA20XX(i2c, address)
|
|
171
|
+
self._fsr = FSR_2_048V
|
|
172
|
+
self._gain = EASYECG_DEFAULT_GAIN
|
|
173
|
+
self._baseline = 0
|
|
174
|
+
|
|
175
|
+
self._adc_rate = EASYECG_DEFAULT_ADC_RATE
|
|
176
|
+
self._out_rate = EASYECG_DEFAULT_OUTPUT_RATE
|
|
177
|
+
self._decim = EASYECG_DEFAULT_ADC_RATE // EASYECG_DEFAULT_OUTPUT_RATE
|
|
178
|
+
self._period_us = 1000000 // EASYECG_DEFAULT_ADC_RATE
|
|
179
|
+
self._next_us = 0
|
|
180
|
+
self._clock_running = False
|
|
181
|
+
self._missed = 0
|
|
182
|
+
|
|
183
|
+
self._notch_on = True
|
|
184
|
+
self._notch_hz = 50.0
|
|
185
|
+
self._notch_q = 4.0
|
|
186
|
+
self._lpf_hz = 40.0
|
|
187
|
+
self._hpf_hz = 0.0
|
|
188
|
+
self._lead_off_uv = _DEFAULT_LEADOFF_UV
|
|
189
|
+
|
|
190
|
+
self._rebuild_filters()
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------- connection
|
|
193
|
+
def begin(self):
|
|
194
|
+
"""AIN0/AIN1, +/-2.048 V, continuous, 3300 SPS -> CONF 0x04C3.
|
|
195
|
+
|
|
196
|
+
Returns False without touching the bus further if the ADC doesn't answer. A write
|
|
197
|
+
to an absent device raises OSError on real I2C, so check first.
|
|
198
|
+
"""
|
|
199
|
+
if not self.is_connected():
|
|
200
|
+
return False
|
|
201
|
+
self.adc.begin()
|
|
202
|
+
self.adc._mux = MUX_AIN0_AIN1
|
|
203
|
+
self.adc._mode = OP_CONTINUOUS
|
|
204
|
+
self.adc._dr = DR_3300SPS
|
|
205
|
+
self._fsr = FSR_2_048V
|
|
206
|
+
self.adc.set_fsr(FSR_2_048V)
|
|
207
|
+
self.reset_filters()
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
def is_connected(self):
|
|
211
|
+
return self.address in self.i2c.scan()
|
|
212
|
+
|
|
213
|
+
def read_config(self):
|
|
214
|
+
"""CONFIG register as read back. Bit 15 reads 0 while a conversion is in progress."""
|
|
215
|
+
return self.adc._read16(_REG_CONFIG)
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------- single samples
|
|
218
|
+
def read_raw_uncorrected(self):
|
|
219
|
+
"""Signed 12-bit counts straight from the ADC."""
|
|
220
|
+
return self.adc.read_adc()
|
|
221
|
+
|
|
222
|
+
def read_raw(self):
|
|
223
|
+
"""Signed 12-bit counts, baseline-corrected."""
|
|
224
|
+
return self.read_raw_uncorrected() - self._baseline
|
|
225
|
+
|
|
226
|
+
def read_voltage(self):
|
|
227
|
+
"""Differential ADC voltage, volts (output-referred)."""
|
|
228
|
+
return self.read_raw() * (self.full_scale_volts() / 2048.0)
|
|
229
|
+
|
|
230
|
+
def read_microvolts(self):
|
|
231
|
+
"""uV at the electrodes, unfiltered."""
|
|
232
|
+
return self._counts_to_uv(self.read_raw())
|
|
233
|
+
|
|
234
|
+
def full_scale_volts(self):
|
|
235
|
+
# Codes 6 and 7 aren't in the table; fall back to +/-2.048 V as Arduino does.
|
|
236
|
+
return _FSR_VOLTS.get(self._fsr, 2.048)
|
|
237
|
+
|
|
238
|
+
def _counts_to_uv(self, counts):
|
|
239
|
+
if self._gain <= 0.0:
|
|
240
|
+
return 0.0
|
|
241
|
+
return counts * (self.full_scale_volts() / 2048.0) * 1000000.0 / self._gain
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------- filter set-up
|
|
244
|
+
def _rebuild_filters(self):
|
|
245
|
+
fs = float(self._adc_rate)
|
|
246
|
+
|
|
247
|
+
# Always designed: its residue is the mains measurement.
|
|
248
|
+
nf = self._notch_hz if self._notch_hz > 0.0 else 50.0
|
|
249
|
+
self._notch = _notch(nf, fs, self._notch_q)
|
|
250
|
+
|
|
251
|
+
if self._lpf_hz > 0.0:
|
|
252
|
+
self._lpf1 = _low_pass(self._lpf_hz, fs, _BW4_Q1)
|
|
253
|
+
self._lpf2 = _low_pass(self._lpf_hz, fs, _BW4_Q2)
|
|
254
|
+
|
|
255
|
+
if self._hpf_hz > 0.0:
|
|
256
|
+
rc = 1.0 / (2.0 * math.pi * self._hpf_hz)
|
|
257
|
+
dt = 1.0 / self._out_rate
|
|
258
|
+
self._hpf_a = rc / (rc + dt)
|
|
259
|
+
else:
|
|
260
|
+
self._hpf_a = 1.0
|
|
261
|
+
|
|
262
|
+
self._sat_alpha = 1.0 / (0.25 * fs)
|
|
263
|
+
self._mains_alpha = 1.0 / (0.5 * fs)
|
|
264
|
+
|
|
265
|
+
self._mwi_len = max(1, min(EASYECG_MWI_MAX, int(0.15 * self._out_rate + 0.5)))
|
|
266
|
+
self.reset_filters()
|
|
267
|
+
|
|
268
|
+
def reset_filters(self):
|
|
269
|
+
"""Clears filter, detector and status state, e.g. after re-attaching electrodes."""
|
|
270
|
+
self._notch.clear()
|
|
271
|
+
if self._lpf_hz > 0.0:
|
|
272
|
+
self._lpf1.clear()
|
|
273
|
+
self._lpf2.clear()
|
|
274
|
+
self._hpf_prev_x = 0.0
|
|
275
|
+
self._hpf_prev_y = 0.0
|
|
276
|
+
|
|
277
|
+
self._decim_count = 0
|
|
278
|
+
self._raw_accum = 0.0
|
|
279
|
+
self._out_ready = False
|
|
280
|
+
self._out_filtered = 0.0
|
|
281
|
+
self._out_unfiltered = 0.0
|
|
282
|
+
|
|
283
|
+
self._sat_level = 0.0
|
|
284
|
+
self._mains_sq = 0.0
|
|
285
|
+
|
|
286
|
+
self._hr_x = [0.0] * 5
|
|
287
|
+
self._mwi_buf = [0.0] * EASYECG_MWI_MAX
|
|
288
|
+
self._mwi_idx = 0
|
|
289
|
+
self._mwi_sum = 0.0
|
|
290
|
+
self._sig_level = 0.0
|
|
291
|
+
self._noise_level = 0.0
|
|
292
|
+
self._hr_count = 0
|
|
293
|
+
self._last_beat = 0
|
|
294
|
+
self._have_beat = False
|
|
295
|
+
self._in_peak = False
|
|
296
|
+
self._peak_val = 0.0
|
|
297
|
+
self._rr = [0] * EASYECG_RR_HISTORY
|
|
298
|
+
self._rr_count = 0
|
|
299
|
+
self._rr_idx = 0
|
|
300
|
+
self._last_rr = 0
|
|
301
|
+
self._bpm = 0.0
|
|
302
|
+
self._beat_flag = False
|
|
303
|
+
|
|
304
|
+
self._clock_running = False
|
|
305
|
+
|
|
306
|
+
def set_sampling(self, adc_rate_hz, output_rate_hz):
|
|
307
|
+
"""adc_rate_hz must be a multiple of output_rate_hz, at most 16 times it.
|
|
308
|
+
|
|
309
|
+
Returns False and keeps the old setting if the pair is invalid.
|
|
310
|
+
"""
|
|
311
|
+
if adc_rate_hz <= 0 or output_rate_hz <= 0 or adc_rate_hz % output_rate_hz:
|
|
312
|
+
return False
|
|
313
|
+
d = adc_rate_hz // output_rate_hz
|
|
314
|
+
if d < 1 or d > EASYECG_MAX_DECIMATION:
|
|
315
|
+
return False
|
|
316
|
+
if self._lpf_hz > 0.0 and self._lpf_hz > 0.45 * adc_rate_hz:
|
|
317
|
+
return False
|
|
318
|
+
if self._notch_hz > 0.0 and adc_rate_hz < self._notch_hz * 2.5:
|
|
319
|
+
return False
|
|
320
|
+
self._adc_rate = adc_rate_hz
|
|
321
|
+
self._out_rate = output_rate_hz
|
|
322
|
+
self._decim = d
|
|
323
|
+
self._period_us = 1000000 // adc_rate_hz
|
|
324
|
+
self._rebuild_filters()
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
def adc_rate_hz(self):
|
|
328
|
+
return self._adc_rate
|
|
329
|
+
|
|
330
|
+
def output_rate_hz(self):
|
|
331
|
+
return self._out_rate
|
|
332
|
+
|
|
333
|
+
def set_notch(self, mains_hz, q=4.0):
|
|
334
|
+
"""Mains notch, 50 or 60 Hz. On by default at 50 Hz. Pass 0 to disable."""
|
|
335
|
+
if mains_hz <= 0.0:
|
|
336
|
+
self._notch_on = False
|
|
337
|
+
self._notch_hz = 0.0
|
|
338
|
+
self._rebuild_filters()
|
|
339
|
+
return True
|
|
340
|
+
if self._adc_rate < mains_hz * 2.5 or q <= 0.0:
|
|
341
|
+
return False
|
|
342
|
+
self._notch_on = True
|
|
343
|
+
self._notch_hz = mains_hz
|
|
344
|
+
self._notch_q = q
|
|
345
|
+
self._rebuild_filters()
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
def notch_enabled(self):
|
|
349
|
+
return self._notch_on
|
|
350
|
+
|
|
351
|
+
def set_low_pass(self, corner_hz):
|
|
352
|
+
"""Digital low-pass corner, Hz. Default 40. Pass 0 to disable."""
|
|
353
|
+
if corner_hz < 0.0 or corner_hz > 0.45 * self._adc_rate:
|
|
354
|
+
return False
|
|
355
|
+
self._lpf_hz = corner_hz
|
|
356
|
+
self._rebuild_filters()
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
def set_high_pass(self, corner_hz):
|
|
360
|
+
"""Digital high-pass corner, Hz. Default 0 = OFF.
|
|
361
|
+
|
|
362
|
+
Off on purpose: the board is already AC-coupled at about 0.5 Hz, and every extra
|
|
363
|
+
high-pass pole distorts the ST segment.
|
|
364
|
+
"""
|
|
365
|
+
self._hpf_hz = corner_hz if corner_hz > 0.0 else 0.0
|
|
366
|
+
self._rebuild_filters()
|
|
367
|
+
|
|
368
|
+
# ------------------------------------------------------------- paced pipeline
|
|
369
|
+
def update(self):
|
|
370
|
+
"""Non-blocking. Call as often as you can.
|
|
371
|
+
|
|
372
|
+
Takes one ADC sample when the sample clock is due and returns True when a new
|
|
373
|
+
output sample is ready.
|
|
374
|
+
"""
|
|
375
|
+
now = ticks_us()
|
|
376
|
+
if not self._clock_running:
|
|
377
|
+
self._clock_running = True
|
|
378
|
+
self._next_us = now
|
|
379
|
+
|
|
380
|
+
late = ticks_diff(now, self._next_us)
|
|
381
|
+
if late < 0:
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
# Advance by whole periods so the rate stays exact. Resync only if badly behind.
|
|
385
|
+
if late > 4 * self._period_us:
|
|
386
|
+
self._missed += late // self._period_us
|
|
387
|
+
self._next_us = now
|
|
388
|
+
self._next_us = ticks_add(self._next_us, self._period_us)
|
|
389
|
+
|
|
390
|
+
self._out_ready = False
|
|
391
|
+
self._process_adc_sample(self.read_raw_uncorrected())
|
|
392
|
+
if self._out_ready:
|
|
393
|
+
self._out_ready = False
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
def _process_adc_sample(self, uncorrected):
|
|
398
|
+
ind = 1.0 if abs(uncorrected) >= EASYECG_SATURATION_COUNTS else 0.0
|
|
399
|
+
self._sat_level += self._sat_alpha * (ind - self._sat_level)
|
|
400
|
+
|
|
401
|
+
x = float(uncorrected - self._baseline)
|
|
402
|
+
|
|
403
|
+
n = self._notch.run(x)
|
|
404
|
+
resid = x - n
|
|
405
|
+
self._mains_sq += self._mains_alpha * ((resid * resid) - self._mains_sq)
|
|
406
|
+
|
|
407
|
+
y = n if self._notch_on else x
|
|
408
|
+
if self._lpf_hz > 0.0:
|
|
409
|
+
y = self._lpf2.run(self._lpf1.run(y))
|
|
410
|
+
|
|
411
|
+
self._raw_accum += x
|
|
412
|
+
self._decim_count += 1
|
|
413
|
+
if self._decim_count >= self._decim:
|
|
414
|
+
self._out_unfiltered = self._raw_accum / self._decim
|
|
415
|
+
self._raw_accum = 0.0
|
|
416
|
+
self._decim_count = 0
|
|
417
|
+
self._process_output_sample(y)
|
|
418
|
+
|
|
419
|
+
def _process_output_sample(self, x):
|
|
420
|
+
if self._hpf_hz > 0.0:
|
|
421
|
+
y = self._hpf_a * (self._hpf_prev_y + x - self._hpf_prev_x)
|
|
422
|
+
self._hpf_prev_x = x
|
|
423
|
+
self._hpf_prev_y = y
|
|
424
|
+
x = y
|
|
425
|
+
self._out_filtered = x
|
|
426
|
+
self._out_ready = True
|
|
427
|
+
self._detect_beat(x)
|
|
428
|
+
|
|
429
|
+
def ecg_filtered(self):
|
|
430
|
+
return _clamp_counts(self._out_filtered)
|
|
431
|
+
|
|
432
|
+
def ecg_microvolts(self):
|
|
433
|
+
return self._counts_to_uv(self._out_filtered)
|
|
434
|
+
|
|
435
|
+
def ecg_unfiltered(self):
|
|
436
|
+
return _clamp_counts(self._out_unfiltered)
|
|
437
|
+
|
|
438
|
+
def ecg_unfiltered_microvolts(self):
|
|
439
|
+
return self._counts_to_uv(self._out_unfiltered)
|
|
440
|
+
|
|
441
|
+
def missed_samples(self):
|
|
442
|
+
return self._missed
|
|
443
|
+
|
|
444
|
+
# ------------------------------------------------------------- heart rate
|
|
445
|
+
def _detect_beat(self, x):
|
|
446
|
+
# Simplified Pan-Tompkins: 5-point derivative -> square -> 150 ms moving-window
|
|
447
|
+
# integral -> adaptive threshold, 200 ms refractory, T-wave rejection inside 360 ms.
|
|
448
|
+
h = self._hr_x
|
|
449
|
+
h[4] = h[3] # one at a time: a 5-way swap builds a tuple
|
|
450
|
+
h[3] = h[2]
|
|
451
|
+
h[2] = h[1]
|
|
452
|
+
h[1] = h[0]
|
|
453
|
+
h[0] = x
|
|
454
|
+
d = ((2.0 * h[0]) + h[1] - h[3] - (2.0 * h[4])) * 0.125
|
|
455
|
+
sq = d * d
|
|
456
|
+
|
|
457
|
+
self._mwi_sum -= self._mwi_buf[self._mwi_idx]
|
|
458
|
+
self._mwi_buf[self._mwi_idx] = sq
|
|
459
|
+
self._mwi_sum += sq
|
|
460
|
+
self._mwi_idx += 1
|
|
461
|
+
if self._mwi_idx >= self._mwi_len:
|
|
462
|
+
self._mwi_idx = 0
|
|
463
|
+
if self._mwi_sum < 0.0:
|
|
464
|
+
self._mwi_sum = 0.0
|
|
465
|
+
mwi = self._mwi_sum / self._mwi_len
|
|
466
|
+
|
|
467
|
+
self._hr_count += 1
|
|
468
|
+
if self._hr_count <= 2 * self._out_rate: # learning phase
|
|
469
|
+
if mwi > self._sig_level:
|
|
470
|
+
self._sig_level = mwi
|
|
471
|
+
self._noise_level += (mwi - self._noise_level) / self._hr_count
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
threshold = self._noise_level + 0.25 * (self._sig_level - self._noise_level)
|
|
475
|
+
since = self._hr_count - self._last_beat
|
|
476
|
+
refractory = int(0.2 * self._out_rate)
|
|
477
|
+
|
|
478
|
+
if not self._in_peak:
|
|
479
|
+
if mwi > threshold and (not self._have_beat or since > refractory):
|
|
480
|
+
self._in_peak = True
|
|
481
|
+
self._peak_val = mwi
|
|
482
|
+
else:
|
|
483
|
+
self._noise_level += 0.01 * (mwi - self._noise_level)
|
|
484
|
+
else:
|
|
485
|
+
if mwi > self._peak_val:
|
|
486
|
+
self._peak_val = mwi
|
|
487
|
+
elif mwi < 0.5 * self._peak_val:
|
|
488
|
+
self._in_peak = False
|
|
489
|
+
rr_ms = (since * 1000) // self._out_rate
|
|
490
|
+
|
|
491
|
+
# A small peak soon after a beat is a T-wave, not a QRS.
|
|
492
|
+
if self._have_beat and rr_ms < 360 and self._peak_val < 0.5 * self._sig_level:
|
|
493
|
+
self._noise_level += 0.125 * (self._peak_val - self._noise_level)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
self._sig_level = 0.125 * self._peak_val + 0.875 * self._sig_level
|
|
497
|
+
|
|
498
|
+
if self._have_beat and 270 <= rr_ms <= 2000: # 30-220 bpm
|
|
499
|
+
self._last_rr = rr_ms
|
|
500
|
+
self._rr[self._rr_idx] = rr_ms
|
|
501
|
+
self._rr_idx = (self._rr_idx + 1) % EASYECG_RR_HISTORY
|
|
502
|
+
if self._rr_count < EASYECG_RR_HISTORY:
|
|
503
|
+
self._rr_count += 1
|
|
504
|
+
total = 0 # no slice: keep allocations out
|
|
505
|
+
rr = self._rr
|
|
506
|
+
for i in range(self._rr_count):
|
|
507
|
+
total += rr[i]
|
|
508
|
+
self._bpm = 60000.0 / (total / self._rr_count)
|
|
509
|
+
|
|
510
|
+
self._have_beat = True
|
|
511
|
+
self._last_beat = self._hr_count
|
|
512
|
+
self._beat_flag = True
|
|
513
|
+
|
|
514
|
+
# No beat for 3 s: let a stale signal level decay. After 5 s, stop reporting a rate.
|
|
515
|
+
if self._have_beat and since > 3 * self._out_rate and not self._in_peak:
|
|
516
|
+
self._sig_level *= 0.98
|
|
517
|
+
if self._sig_level < self._noise_level:
|
|
518
|
+
self._sig_level = self._noise_level
|
|
519
|
+
if self._have_beat and since > 5 * self._out_rate:
|
|
520
|
+
self._bpm = 0.0
|
|
521
|
+
self._rr_count = 0
|
|
522
|
+
self._rr_idx = 0
|
|
523
|
+
|
|
524
|
+
def heart_rate(self):
|
|
525
|
+
"""Beats per minute, 0 until established (about 2 s to learn, then a few beats)."""
|
|
526
|
+
return self._bpm
|
|
527
|
+
|
|
528
|
+
def beat_detected(self):
|
|
529
|
+
"""True once per detected beat; clears when read."""
|
|
530
|
+
b = self._beat_flag
|
|
531
|
+
self._beat_flag = False
|
|
532
|
+
return b
|
|
533
|
+
|
|
534
|
+
def last_rr_millis(self):
|
|
535
|
+
"""Most recent R-R interval, ms (Arduino lastRRMillis())."""
|
|
536
|
+
return self._last_rr
|
|
537
|
+
|
|
538
|
+
last_rr_ms = last_rr_millis # 0.1.0 name, kept for compatibility
|
|
539
|
+
|
|
540
|
+
# ------------------------------------------------------------- signal quality
|
|
541
|
+
def is_saturated(self):
|
|
542
|
+
"""Pinned near a rail for most of the last ~250 ms.
|
|
543
|
+
|
|
544
|
+
On a mains-powered host this is the driven-right-leg failure: use a battery or an
|
|
545
|
+
easyISO. It is also what a floating electrode looks like.
|
|
546
|
+
"""
|
|
547
|
+
return self._sat_level > 0.5
|
|
548
|
+
|
|
549
|
+
def mains_microvolts(self):
|
|
550
|
+
"""Mains pickup, uV RMS at the electrodes: what the notch is removing."""
|
|
551
|
+
return self._counts_to_uv(math.sqrt(self._mains_sq))
|
|
552
|
+
|
|
553
|
+
def is_lead_off(self):
|
|
554
|
+
"""PROVISIONAL heuristic: saturated, or mains pickup above the threshold."""
|
|
555
|
+
return self.is_saturated() or (
|
|
556
|
+
self._lead_off_uv > 0.0 and self.mains_microvolts() > self._lead_off_uv)
|
|
557
|
+
|
|
558
|
+
def set_lead_off_threshold(self, mains_uv_rms):
|
|
559
|
+
self._lead_off_uv = mains_uv_rms
|
|
560
|
+
|
|
561
|
+
def get_lead_off_threshold(self):
|
|
562
|
+
return self._lead_off_uv
|
|
563
|
+
|
|
564
|
+
# ------------------------------------------------------------- baseline and gain
|
|
565
|
+
def calibrate_baseline(self, duration_ms=1000):
|
|
566
|
+
"""Average the uncorrected reading over whole heartbeats. Electrodes on, sit still."""
|
|
567
|
+
if duration_ms <= 0:
|
|
568
|
+
return
|
|
569
|
+
# Average the UNCORRECTED count so repeated calls converge instead of stacking.
|
|
570
|
+
total = 0
|
|
571
|
+
n = 0
|
|
572
|
+
t0 = ticks_us()
|
|
573
|
+
while ticks_diff(ticks_us(), t0) < duration_ms * 1000:
|
|
574
|
+
total += self.read_raw_uncorrected()
|
|
575
|
+
n += 1
|
|
576
|
+
sleep_us(500)
|
|
577
|
+
if n:
|
|
578
|
+
self._baseline = int(total / n) # truncates toward zero, as in C
|
|
579
|
+
self.reset_filters()
|
|
580
|
+
|
|
581
|
+
def set_baseline(self, offset_counts):
|
|
582
|
+
self._baseline = offset_counts
|
|
583
|
+
self.reset_filters()
|
|
584
|
+
|
|
585
|
+
def get_baseline(self):
|
|
586
|
+
return self._baseline
|
|
587
|
+
|
|
588
|
+
def set_gain(self, gain):
|
|
589
|
+
"""Python-only. Arduino fixes the gain at EASYECG_GAIN (set by R10, R14, R19).
|
|
590
|
+
|
|
591
|
+
Pass a measured gain to correct absolute microvolts. Ignored unless > 0.
|
|
592
|
+
"""
|
|
593
|
+
if gain > 0.0:
|
|
594
|
+
self._gain = gain
|
|
595
|
+
|
|
596
|
+
def get_gain(self):
|
|
597
|
+
return self._gain
|
|
598
|
+
|
|
599
|
+
# ------------------------------------------------------------- low-level passthroughs
|
|
600
|
+
def set_data_rate(self, rate):
|
|
601
|
+
"""ADC conversion rate, a DR_* code. begin() sets DR_3300SPS.
|
|
602
|
+
|
|
603
|
+
Leave it there for the paced pipeline: the conversion must outrun the ADC rate
|
|
604
|
+
set with set_sampling(), or update() reads the same conversion twice.
|
|
605
|
+
"""
|
|
606
|
+
self.adc.set_dr(rate)
|
|
607
|
+
|
|
608
|
+
def set_range(self, fsr):
|
|
609
|
+
"""Change the PGA range, an FSR_* code. +/-2.048 V is the ECG range; see the README."""
|
|
610
|
+
self._fsr = fsr
|
|
611
|
+
self.adc.set_fsr(fsr)
|
|
612
|
+
self.reset_filters()
|
|
613
|
+
|
|
614
|
+
def set_mode(self, mode):
|
|
615
|
+
"""MODE_CONTINUOUS (begin() default) or MODE_SINGLE_SHOT."""
|
|
616
|
+
self.adc.set_mode(mode)
|
|
617
|
+
|
|
618
|
+
def set_mux(self, mux):
|
|
619
|
+
"""Input mux, a MUX_* code. begin() sets MUX_AIN0_AIN1 (signal vs mid-rail)."""
|
|
620
|
+
self.adc.set_mux(mux)
|
|
621
|
+
|
|
622
|
+
def read_single_shot(self):
|
|
623
|
+
"""Start one conversion, wait for it, return baseline-corrected counts.
|
|
624
|
+
|
|
625
|
+
As in Arduino, this leaves the ADC in single-shot mode: call
|
|
626
|
+
set_mode(MODE_CONTINUOUS) or begin() before using update() again.
|
|
627
|
+
"""
|
|
628
|
+
adc = self.adc
|
|
629
|
+
adc._mode = OP_SINGLE
|
|
630
|
+
adc._write16(_REG_CONFIG, adc._conf_word() | 0x8000)
|
|
631
|
+
wait = _CONV_US[adc._dr] if 0 <= adc._dr < len(_CONV_US) else _CONV_US[0]
|
|
632
|
+
sleep_us(2 * wait)
|
|
633
|
+
return self.read_raw()
|