protocentral-easyemg 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_easyemg-0.2.0/.gitignore +6 -0
- protocentral_easyemg-0.2.0/CHANGELOG.md +41 -0
- protocentral_easyemg-0.2.0/LICENSE.md +21 -0
- protocentral_easyemg-0.2.0/PKG-INFO +127 -0
- protocentral_easyemg-0.2.0/README.md +113 -0
- protocentral_easyemg-0.2.0/examples/easyemg_activation_led.py +116 -0
- protocentral_easyemg-0.2.0/examples/easyemg_calibration.py +68 -0
- protocentral_easyemg-0.2.0/examples/easyemg_diagnostics.py +346 -0
- protocentral_easyemg-0.2.0/examples/easyemg_envelope.py +100 -0
- protocentral_easyemg-0.2.0/examples/easyemg_openview.py +189 -0
- protocentral_easyemg-0.2.0/examples/easyemg_plotter.py +43 -0
- protocentral_easyemg-0.2.0/examples/easyemg_raspberrypi.py +32 -0
- protocentral_easyemg-0.2.0/examples/easyemg_simpletest.py +35 -0
- protocentral_easyemg-0.2.0/protocentral_easyemg.py +694 -0
- protocentral_easyemg-0.2.0/pyproject.toml +31 -0
- protocentral_easyemg-0.2.0/tests/test_easyemg.py +722 -0
|
@@ -0,0 +1,41 @@
|
|
|
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 easyEMG Arduino library](https://github.com/Protocentral/protocentral_easyemg_arduino)
|
|
11
|
+
1.1.0 for MicroPython and Raspberry Pi.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- The Arduino API in snake_case: raw sEMG in µV at the electrodes, sliding-window RMS
|
|
15
|
+
envelope, baseline zeroing, activation threshold and %MVC.
|
|
16
|
+
- `set_mains_notch()`, a notch on every mains harmonic up to 0.45× the sample rate
|
|
17
|
+
(6 Hz wide at the fundamental, 2 Hz on the harmonics), the single `set_notch()`, and
|
|
18
|
+
a 2nd-order digital high-pass. All off by default.
|
|
19
|
+
- Paced sampling: `update()` on a fixed clock (500 SPS by default), with
|
|
20
|
+
`missed_samples()` and `reset_clock()`.
|
|
21
|
+
- `MvcRecorder`, the MVC recording step from the Arduino `06.OpenView` sketch.
|
|
22
|
+
- Low-level ADC passthroughs: `set_range()`, `set_data_rate()`, `set_mode()`,
|
|
23
|
+
`set_mux()` and `read_single_shot()`.
|
|
24
|
+
- Python-only extras: `set_gain()`, `microvolts_per_count()`, `last_envelope_raw()`
|
|
25
|
+
and `.adc`.
|
|
26
|
+
- An example for every Arduino example: `easyemg_simpletest.py` and
|
|
27
|
+
`easyemg_raspberrypi.py` (`01`), `easyemg_plotter.py` (`02`),
|
|
28
|
+
`easyemg_calibration.py` (`03`), `easyemg_envelope.py` (`04`),
|
|
29
|
+
`easyemg_activation_led.py` (`05`), `easyemg_openview.py` (`06`, the same packets for
|
|
30
|
+
OpenView's easyEMG Breakout profile) and `easyemg_diagnostics.py` (`07`).
|
|
31
|
+
- Mocked-bus tests, including each example run end to end, and an `mpy-cross` compile
|
|
32
|
+
in CI, on every push and before every PyPI publish.
|
|
33
|
+
|
|
34
|
+
### Differences from the Arduino library
|
|
35
|
+
- The driver paces sampling itself, at 500 SPS by default, where the Arduino sketches
|
|
36
|
+
pace at 1000 SPS. `easyemg_openview.py` therefore sends one sample per packet rather
|
|
37
|
+
than averaging two, and its harmonic notch covers 50–200 Hz.
|
|
38
|
+
|
|
39
|
+
Validated with the mocked-bus tests. On an ESP32-S3 (MicroPython 1.29) the driver holds
|
|
40
|
+
500 SPS through the full filter chain with no missed samples. Further bench results
|
|
41
|
+
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,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: protocentral-easyemg
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MicroPython + Raspberry Pi driver for the Protocentral easyEMG surface EMG breakout (TLA2024-based)
|
|
5
|
+
Project-URL: Homepage, https://protocentral.com
|
|
6
|
+
Project-URL: Source, https://github.com/Protocentral/protocentral-micropython-easyemg
|
|
7
|
+
Author-email: Protocentral Electronics <info@protocentral.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Keywords: easyemg,emg,i2c,micropython,protocentral,qwiic,raspberry-pi,semg,tla2024
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: protocentral-tla20xx>=1.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Protocentral easyEMG MicroPython Library
|
|
16
|
+
|
|
17
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyemg/actions/workflows/test.yml)
|
|
18
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyemg/actions/workflows/publish.yml)
|
|
19
|
+
|
|
20
|
+
MicroPython library for the **Protocentral easyEMG Breakout**: single-channel surface EMG (sEMG) over I²C / Qwiic. It gives you the raw band-limited waveform and computes the RMS envelope in software. The same driver also runs on a Raspberry Pi.
|
|
21
|
+
|
|
22
|
+
## Don't have one? [Buy it here](https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/)
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**Wiring, electrode placement and troubleshooting are in the [easyEMG Getting Started guide](https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/docs/getting-started/).**
|
|
27
|
+
|
|
28
|
+
## Links
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Product page | https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/ |
|
|
33
|
+
| Getting Started guide | https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/docs/getting-started/ |
|
|
34
|
+
| Arduino library | https://github.com/Protocentral/protocentral_easyemg_arduino |
|
|
35
|
+
| Hardware design files | https://github.com/Protocentral/protocentral_easyemg_hardware |
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Raw sEMG in µV at the electrodes, on a paced 500 SPS clock with missed-sample counting
|
|
40
|
+
- Sliding-window RMS envelope, activation threshold and %MVC
|
|
41
|
+
- `MvcRecorder`, the same MVC recording step as the Arduino OpenView sketch
|
|
42
|
+
- Optional mains notch (fundamental or every harmonic) and digital high-pass
|
|
43
|
+
- A port of the [Arduino library](https://github.com/Protocentral/protocentral_easyemg_arduino) 1.1.0: the same API in snake_case, and a Python version of every Arduino example
|
|
44
|
+
|
|
45
|
+
> **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 no EMG at all.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
**MicroPython** (Raspberry Pi Pico, ESP32, …):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mpremote mip install github:Protocentral/protocentral-micropython-easyemg
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This also installs the [TLA20xx driver](https://github.com/Protocentral/protocentral-micropython-tla20xx). With Thonny, copy `protocentral_easyemg.py` and `protocentral_tla20xx.py` to the board instead.
|
|
56
|
+
|
|
57
|
+
**Raspberry Pi**, with I²C enabled (`sudo raspi-config nonint do_i2c 0`):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python3 -m venv ~/easyemg && source ~/easyemg/bin/activate
|
|
61
|
+
pip install protocentral-easyemg
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from machine import Pin, I2C
|
|
68
|
+
from protocentral_easyemg import EasyEMG
|
|
69
|
+
|
|
70
|
+
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000) # Pico: GP5, GP4. Change for your board
|
|
71
|
+
emg = EasyEMG(i2c) # default address 0x48
|
|
72
|
+
if not emg.begin():
|
|
73
|
+
raise OSError("easyEMG not found")
|
|
74
|
+
emg.calibrate_baseline() # muscle relaxed, electrodes on
|
|
75
|
+
|
|
76
|
+
n = 0
|
|
77
|
+
while True:
|
|
78
|
+
if emg.update(): # True once per sample, 500 SPS
|
|
79
|
+
n += 1
|
|
80
|
+
if n % 50 == 0: # print 10 times a second
|
|
81
|
+
print(emg.last_envelope(), emg.missed_samples())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
On a Raspberry Pi, replace the I²C lines with `from protocentral_tla20xx_linux import I2C` and `EasyEMG(I2C(bus=1))`.
|
|
85
|
+
|
|
86
|
+
Call `update()` as often as you can; it samples only when the clock is due, and never blocks. If `missed_samples()` grows, print less or lower the rate with `set_sample_rate()`. Filters need that same rate: `set_mains_notch(50, emg.sample_rate_hz())`.
|
|
87
|
+
|
|
88
|
+
## API Reference
|
|
89
|
+
|
|
90
|
+
| Method | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `EasyEMG(i2c, address=0x48)` / `begin()` | Construct / configure the ADC. `begin()` returns `False` if the board doesn't answer |
|
|
93
|
+
| `update()` | Non-blocking. Takes one sample when the clock is due; `True` if it did |
|
|
94
|
+
| `last_microvolts()` / `last_envelope()` | The latest sample and RMS envelope, in µV at the electrodes |
|
|
95
|
+
| `set_sample_rate(hz)` / `missed_samples()` | Paced clock, default 500 SPS / samples it couldn't take on time |
|
|
96
|
+
| `set_envelope_window(n)` | 1–128 samples, default 64 (128 ms at 500 SPS) |
|
|
97
|
+
| `set_mains_notch(hz, fs)` / `set_notch(hz, fs)` / `set_high_pass(hz, fs)` | Optional filters, off by default |
|
|
98
|
+
| `calibrate_baseline(n)` / `set_baseline(n)` | Measure and subtract the resting offset |
|
|
99
|
+
| `set_activation_threshold(uv)` / `is_active()` | Compare the envelope with a threshold |
|
|
100
|
+
| `set_mvc(uv)` / `read_percent_mvc()` / `MvcRecorder` | %MVC normalisation and the MVC recording step |
|
|
101
|
+
|
|
102
|
+
The full API, including the unpaced reads and low-level ADC passthroughs, is documented in [protocentral_easyemg.py](https://github.com/Protocentral/protocentral-micropython-easyemg/blob/main/protocentral_easyemg.py).
|
|
103
|
+
|
|
104
|
+
## Examples
|
|
105
|
+
|
|
106
|
+
| # | Example | Description |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| 01 | `easyemg_simpletest.py` / `easyemg_raspberrypi.py` | Raw µV and envelope (MicroPython / Raspberry Pi) |
|
|
109
|
+
| 02 | `easyemg_plotter.py` | sEMG and envelope for Thonny's plotter |
|
|
110
|
+
| 03 | `easyemg_calibration.py` | Baseline zeroing and your resting noise floor |
|
|
111
|
+
| 04 | `easyemg_envelope.py` | RMS envelope, MVC recording and %MVC |
|
|
112
|
+
| 05 | `easyemg_activation_led.py` | Flex to light an LED, with an automatic threshold and hysteresis |
|
|
113
|
+
| 06 | `easyemg_openview.py` | Stream to [OpenView](https://github.com/Protocentral/protocentral_openview) 3.4+ (**easyEMG Breakout**), with the MVC step. Save it to the board as `main.py`, then close Thonny or `mpremote` before connecting OpenView |
|
|
114
|
+
| 07 | `easyemg_diagnostics.py` | I²C, ADC, sample-rate, noise and mains-hum checks. **Run this first if something is wrong** |
|
|
115
|
+
|
|
116
|
+
The examples default to QT Py ESP32-C3 pins: change the `I2C(...)` line for your board.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
- **Software:** MIT, see [LICENSE.md](https://github.com/Protocentral/protocentral-micropython-easyemg/blob/main/LICENSE.md)
|
|
121
|
+
- **Hardware:** CERN-OHL-P v2, in the [hardware repo](https://github.com/Protocentral/protocentral_easyemg_hardware)
|
|
122
|
+
|
|
123
|
+
Copyright (c) 2026 Protocentral Electronics.
|
|
124
|
+
|
|
125
|
+
## Support
|
|
126
|
+
|
|
127
|
+
Open an issue on [GitHub](https://github.com/Protocentral/protocentral-micropython-easyemg/issues) or email support@protocentral.com.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Protocentral easyEMG MicroPython Library
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyemg/actions/workflows/test.yml)
|
|
4
|
+
[](https://github.com/Protocentral/protocentral-micropython-easyemg/actions/workflows/publish.yml)
|
|
5
|
+
|
|
6
|
+
MicroPython library for the **Protocentral easyEMG Breakout**: single-channel surface EMG (sEMG) over I²C / Qwiic. It gives you the raw band-limited waveform and computes the RMS envelope in software. The same driver also runs on a Raspberry Pi.
|
|
7
|
+
|
|
8
|
+
## Don't have one? [Buy it here](https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/)
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
**Wiring, electrode placement and troubleshooting are in the [easyEMG Getting Started guide](https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/docs/getting-started/).**
|
|
13
|
+
|
|
14
|
+
## Links
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Product page | https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/ |
|
|
19
|
+
| Getting Started guide | https://protocentral.com/product/easyemg-surface-emg-breakout-qwiic/docs/getting-started/ |
|
|
20
|
+
| Arduino library | https://github.com/Protocentral/protocentral_easyemg_arduino |
|
|
21
|
+
| Hardware design files | https://github.com/Protocentral/protocentral_easyemg_hardware |
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Raw sEMG in µV at the electrodes, on a paced 500 SPS clock with missed-sample counting
|
|
26
|
+
- Sliding-window RMS envelope, activation threshold and %MVC
|
|
27
|
+
- `MvcRecorder`, the same MVC recording step as the Arduino OpenView sketch
|
|
28
|
+
- Optional mains notch (fundamental or every harmonic) and digital high-pass
|
|
29
|
+
- A port of the [Arduino library](https://github.com/Protocentral/protocentral_easyemg_arduino) 1.1.0: the same API in snake_case, and a Python version of every Arduino example
|
|
30
|
+
|
|
31
|
+
> **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 no EMG at all.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
**MicroPython** (Raspberry Pi Pico, ESP32, …):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mpremote mip install github:Protocentral/protocentral-micropython-easyemg
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This also installs the [TLA20xx driver](https://github.com/Protocentral/protocentral-micropython-tla20xx). With Thonny, copy `protocentral_easyemg.py` and `protocentral_tla20xx.py` to the board instead.
|
|
42
|
+
|
|
43
|
+
**Raspberry Pi**, with I²C enabled (`sudo raspi-config nonint do_i2c 0`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 -m venv ~/easyemg && source ~/easyemg/bin/activate
|
|
47
|
+
pip install protocentral-easyemg
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from machine import Pin, I2C
|
|
54
|
+
from protocentral_easyemg import EasyEMG
|
|
55
|
+
|
|
56
|
+
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000) # Pico: GP5, GP4. Change for your board
|
|
57
|
+
emg = EasyEMG(i2c) # default address 0x48
|
|
58
|
+
if not emg.begin():
|
|
59
|
+
raise OSError("easyEMG not found")
|
|
60
|
+
emg.calibrate_baseline() # muscle relaxed, electrodes on
|
|
61
|
+
|
|
62
|
+
n = 0
|
|
63
|
+
while True:
|
|
64
|
+
if emg.update(): # True once per sample, 500 SPS
|
|
65
|
+
n += 1
|
|
66
|
+
if n % 50 == 0: # print 10 times a second
|
|
67
|
+
print(emg.last_envelope(), emg.missed_samples())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
On a Raspberry Pi, replace the I²C lines with `from protocentral_tla20xx_linux import I2C` and `EasyEMG(I2C(bus=1))`.
|
|
71
|
+
|
|
72
|
+
Call `update()` as often as you can; it samples only when the clock is due, and never blocks. If `missed_samples()` grows, print less or lower the rate with `set_sample_rate()`. Filters need that same rate: `set_mains_notch(50, emg.sample_rate_hz())`.
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
| Method | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `EasyEMG(i2c, address=0x48)` / `begin()` | Construct / configure the ADC. `begin()` returns `False` if the board doesn't answer |
|
|
79
|
+
| `update()` | Non-blocking. Takes one sample when the clock is due; `True` if it did |
|
|
80
|
+
| `last_microvolts()` / `last_envelope()` | The latest sample and RMS envelope, in µV at the electrodes |
|
|
81
|
+
| `set_sample_rate(hz)` / `missed_samples()` | Paced clock, default 500 SPS / samples it couldn't take on time |
|
|
82
|
+
| `set_envelope_window(n)` | 1–128 samples, default 64 (128 ms at 500 SPS) |
|
|
83
|
+
| `set_mains_notch(hz, fs)` / `set_notch(hz, fs)` / `set_high_pass(hz, fs)` | Optional filters, off by default |
|
|
84
|
+
| `calibrate_baseline(n)` / `set_baseline(n)` | Measure and subtract the resting offset |
|
|
85
|
+
| `set_activation_threshold(uv)` / `is_active()` | Compare the envelope with a threshold |
|
|
86
|
+
| `set_mvc(uv)` / `read_percent_mvc()` / `MvcRecorder` | %MVC normalisation and the MVC recording step |
|
|
87
|
+
|
|
88
|
+
The full API, including the unpaced reads and low-level ADC passthroughs, is documented in [protocentral_easyemg.py](https://github.com/Protocentral/protocentral-micropython-easyemg/blob/main/protocentral_easyemg.py).
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
| # | Example | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 01 | `easyemg_simpletest.py` / `easyemg_raspberrypi.py` | Raw µV and envelope (MicroPython / Raspberry Pi) |
|
|
95
|
+
| 02 | `easyemg_plotter.py` | sEMG and envelope for Thonny's plotter |
|
|
96
|
+
| 03 | `easyemg_calibration.py` | Baseline zeroing and your resting noise floor |
|
|
97
|
+
| 04 | `easyemg_envelope.py` | RMS envelope, MVC recording and %MVC |
|
|
98
|
+
| 05 | `easyemg_activation_led.py` | Flex to light an LED, with an automatic threshold and hysteresis |
|
|
99
|
+
| 06 | `easyemg_openview.py` | Stream to [OpenView](https://github.com/Protocentral/protocentral_openview) 3.4+ (**easyEMG Breakout**), with the MVC step. Save it to the board as `main.py`, then close Thonny or `mpremote` before connecting OpenView |
|
|
100
|
+
| 07 | `easyemg_diagnostics.py` | I²C, ADC, sample-rate, noise and mains-hum checks. **Run this first if something is wrong** |
|
|
101
|
+
|
|
102
|
+
The examples default to QT Py ESP32-C3 pins: change the `I2C(...)` line for your board.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
- **Software:** MIT, see [LICENSE.md](https://github.com/Protocentral/protocentral-micropython-easyemg/blob/main/LICENSE.md)
|
|
107
|
+
- **Hardware:** CERN-OHL-P v2, in the [hardware repo](https://github.com/Protocentral/protocentral_easyemg_hardware)
|
|
108
|
+
|
|
109
|
+
Copyright (c) 2026 Protocentral Electronics.
|
|
110
|
+
|
|
111
|
+
## Support
|
|
112
|
+
|
|
113
|
+
Open an issue on [GitHub](https://github.com/Protocentral/protocentral-micropython-easyemg/issues) or email support@protocentral.com.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
|
|
3
|
+
#
|
|
4
|
+
# easyemg_activation_led.py - Arduino counterpart: 05.MuscleActivation_LED
|
|
5
|
+
#
|
|
6
|
+
# Flex the muscle, the LED lights. Runs on MicroPython and on a Raspberry Pi.
|
|
7
|
+
#
|
|
8
|
+
# The classic muscle-sensor demo, and the starting point for prosthetics and gesture
|
|
9
|
+
# control. The threshold is set from YOUR resting noise floor rather than hard-coded,
|
|
10
|
+
# because the right number depends on electrode placement, skin prep and the person: the
|
|
11
|
+
# script measures the resting envelope, then triggers at a multiple of it.
|
|
12
|
+
#
|
|
13
|
+
# Hysteresis matters. A single threshold makes the LED chatter whenever the envelope sits
|
|
14
|
+
# near it, so this uses separate ON and OFF levels: a Schmitt trigger, in software.
|
|
15
|
+
#
|
|
16
|
+
# FLEX / relax is printed either way, so it works with no LED. Set LED_PIN for yours.
|
|
17
|
+
#
|
|
18
|
+
# For research and education only - not a medical device. With electrodes on a person,
|
|
19
|
+
# power the host from a battery or connect through an easyISO isolated Qwiic adapter.
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from machine import Pin, I2C
|
|
23
|
+
# Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
|
|
24
|
+
i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
|
|
25
|
+
except ImportError: # Raspberry Pi
|
|
26
|
+
from protocentral_tla20xx_linux import I2C
|
|
27
|
+
i2c = I2C(bus=1)
|
|
28
|
+
Pin = None
|
|
29
|
+
|
|
30
|
+
from protocentral_easyemg import EasyEMG, sleep_ms
|
|
31
|
+
|
|
32
|
+
# MicroPython: "LED" on a Pico / Pico W, 2 on many ESP32 dev boards. The QT Py ESP32-C3
|
|
33
|
+
# has only an RGB NeoPixel, so wire an LED (with a resistor) to a free pin.
|
|
34
|
+
# Raspberry Pi: a BCM pin number, driven through gpiozero.
|
|
35
|
+
LED_PIN = None
|
|
36
|
+
|
|
37
|
+
TRIGGER_MULTIPLE = 4.0 # ON at 4x the resting noise floor ...
|
|
38
|
+
RELEASE_MULTIPLE = 2.5 # ... OFF below 2.5x
|
|
39
|
+
ENVELOPE_WINDOW = 32 # samples: 64 ms at 500 SPS, quicker to react
|
|
40
|
+
REST_SECONDS = 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def make_led(pin):
|
|
44
|
+
"""A function that sets the LED on (1) or off (0), or None with no LED."""
|
|
45
|
+
if pin is None:
|
|
46
|
+
return None
|
|
47
|
+
if Pin is not None:
|
|
48
|
+
p = Pin(pin, Pin.OUT)
|
|
49
|
+
return p.value
|
|
50
|
+
from gpiozero import LED # Raspberry Pi
|
|
51
|
+
led = LED(pin)
|
|
52
|
+
return lambda v: led.on() if v else led.off()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
set_led = make_led(LED_PIN)
|
|
56
|
+
if set_led:
|
|
57
|
+
set_led(0)
|
|
58
|
+
|
|
59
|
+
emg = EasyEMG(i2c) # ships at 0x48
|
|
60
|
+
if not emg.begin():
|
|
61
|
+
raise OSError("easyEMG not found - check wiring and I2C address (0x48)")
|
|
62
|
+
|
|
63
|
+
print("Relax the muscle. Measuring your resting noise floor...")
|
|
64
|
+
sleep_ms(1000)
|
|
65
|
+
emg.calibrate_baseline()
|
|
66
|
+
emg.set_envelope_window(ENVELOPE_WINDOW)
|
|
67
|
+
|
|
68
|
+
# Average the resting envelope over a couple of seconds rather than trusting one reading.
|
|
69
|
+
total = 0.0
|
|
70
|
+
n = 0
|
|
71
|
+
target = REST_SECONDS * emg.sample_rate_hz()
|
|
72
|
+
while n < target:
|
|
73
|
+
if emg.update():
|
|
74
|
+
total += emg.last_envelope()
|
|
75
|
+
n += 1
|
|
76
|
+
resting = total / n
|
|
77
|
+
|
|
78
|
+
# A dead-quiet (or stuck) ADC gives a resting level of 0, and a 0 threshold would light the
|
|
79
|
+
# LED permanently. Hold the floor at one ADC count.
|
|
80
|
+
floor = emg.microvolts_per_count()
|
|
81
|
+
if resting < floor:
|
|
82
|
+
print("Resting level is under one ADC count. If every reading stays at 0, run")
|
|
83
|
+
print("easyemg_diagnostics.py: step 4 tells a quiet board from a stuck ADC.")
|
|
84
|
+
resting = floor
|
|
85
|
+
|
|
86
|
+
on_threshold = resting * TRIGGER_MULTIPLE
|
|
87
|
+
off_threshold = resting * RELEASE_MULTIPLE
|
|
88
|
+
emg.set_activation_threshold(on_threshold)
|
|
89
|
+
|
|
90
|
+
print("Resting level : {:.2f} uV RMS".format(resting))
|
|
91
|
+
print("Trigger at : {:.2f} uV RMS".format(on_threshold))
|
|
92
|
+
print("Release at : {:.2f} uV RMS".format(off_threshold))
|
|
93
|
+
print()
|
|
94
|
+
print("Flex the muscle.")
|
|
95
|
+
|
|
96
|
+
muscle_on = False
|
|
97
|
+
while True:
|
|
98
|
+
if not emg.update():
|
|
99
|
+
continue
|
|
100
|
+
envelope = emg.last_envelope()
|
|
101
|
+
|
|
102
|
+
# Schmitt trigger: different levels for turning on and off.
|
|
103
|
+
if not muscle_on and envelope >= on_threshold:
|
|
104
|
+
muscle_on = True
|
|
105
|
+
if set_led:
|
|
106
|
+
set_led(1)
|
|
107
|
+
print("FLEX {:.1f} uV".format(envelope))
|
|
108
|
+
elif muscle_on and envelope < off_threshold:
|
|
109
|
+
muscle_on = False
|
|
110
|
+
if set_led:
|
|
111
|
+
set_led(0)
|
|
112
|
+
print("relax")
|
|
113
|
+
|
|
114
|
+
# emg.is_active() is the simple single-threshold version, if you don't want
|
|
115
|
+
# hysteresis:
|
|
116
|
+
# if emg.is_active(): ...
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Ashwin Whitchurch, Protocentral Electronics
|
|
3
|
+
#
|
|
4
|
+
# easyemg_calibration.py - Arduino counterpart: 03.Calibration
|
|
5
|
+
#
|
|
6
|
+
# Zeroes the resting offset and measures your noise floor. Runs on MicroPython and on a
|
|
7
|
+
# Raspberry Pi.
|
|
8
|
+
#
|
|
9
|
+
# Do this every session, per subject. The board reads differentially against its own
|
|
10
|
+
# buffered mid-rail, so the resting count is already near zero, but amplifier offset leaves
|
|
11
|
+
# a few counts behind. calibrate_baseline() averages the resting signal and subtracts it.
|
|
12
|
+
#
|
|
13
|
+
# The front-end gain (x51.7) is set by the onboard resistors, so there is nothing to
|
|
14
|
+
# calibrate there. The script prints it for reference.
|
|
15
|
+
#
|
|
16
|
+
# With the muscle relaxed, the resting envelope should sit close to the board's resolution
|
|
17
|
+
# (a few uV). If it is much higher, suspect mains pickup, long electrode leads or poor skin
|
|
18
|
+
# prep, and run easyemg_diagnostics.py.
|
|
19
|
+
#
|
|
20
|
+
# For research and education only - not a medical device. With electrodes on a person,
|
|
21
|
+
# power the host from a battery or connect through an easyISO isolated Qwiic adapter.
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from machine import Pin, I2C
|
|
25
|
+
# Defaults for the Adafruit QT Py ESP32-C3 (SCL=6, SDA=5). Change for your board.
|
|
26
|
+
i2c = I2C(0, scl=Pin(6), sda=Pin(5), freq=400000)
|
|
27
|
+
except ImportError: # Raspberry Pi
|
|
28
|
+
from protocentral_tla20xx_linux import I2C
|
|
29
|
+
i2c = I2C(bus=1)
|
|
30
|
+
|
|
31
|
+
from protocentral_easyemg import EasyEMG, sleep_ms
|
|
32
|
+
|
|
33
|
+
BASELINE_SAMPLES = 512
|
|
34
|
+
ENVELOPE_WINDOW = 64 # samples: 128 ms at 500 SPS
|
|
35
|
+
NOISE_SECONDS = 2
|
|
36
|
+
|
|
37
|
+
emg = EasyEMG(i2c) # ships at 0x48
|
|
38
|
+
if not emg.begin():
|
|
39
|
+
raise OSError("easyEMG not found - check wiring and I2C address (0x48)")
|
|
40
|
+
|
|
41
|
+
print("=== easyEMG calibration ===")
|
|
42
|
+
print("Front-end gain : x{:.2f}".format(emg.get_gain()))
|
|
43
|
+
print("ADC full scale : +/-{:.3f} V".format(emg.full_scale_volts()))
|
|
44
|
+
print("Resolution at the skin: {:.2f} uV per count".format(emg.microvolts_per_count()))
|
|
45
|
+
print()
|
|
46
|
+
|
|
47
|
+
print("Relax the muscle completely. Measuring baseline in 3 seconds...")
|
|
48
|
+
sleep_ms(3000)
|
|
49
|
+
emg.calibrate_baseline(BASELINE_SAMPLES)
|
|
50
|
+
print("Baseline offset: {} counts".format(emg.get_baseline()))
|
|
51
|
+
print()
|
|
52
|
+
|
|
53
|
+
# Paced on the driver's clock, so the envelope covers a known time span.
|
|
54
|
+
print("Measuring resting noise floor ({} s)...".format(NOISE_SECONDS))
|
|
55
|
+
emg.set_envelope_window(ENVELOPE_WINDOW)
|
|
56
|
+
n = 0
|
|
57
|
+
target = NOISE_SECONDS * emg.sample_rate_hz()
|
|
58
|
+
while n < target:
|
|
59
|
+
if emg.update():
|
|
60
|
+
n += 1
|
|
61
|
+
print("Resting envelope: {:.2f} uV RMS".format(emg.last_envelope()))
|
|
62
|
+
if emg.missed_samples():
|
|
63
|
+
print("({} samples missed - the host couldn't hold {} SPS)".format(
|
|
64
|
+
emg.missed_samples(), emg.sample_rate_hz()))
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
print("Copy this line into your own script, after emg.begin():")
|
|
68
|
+
print(" emg.set_baseline({})".format(emg.get_baseline()))
|