Skip to content
Snippets Groups Projects
Commit 3f1c3b98 authored by schneider's avatar schneider
Browse files

Merge branch 'schneider/ecg-streaming' into 'master'

feat(ecg): Experimental streaming of ECG samples via BLE

See merge request card10/firmware!493
parents 6ccfc6dd b0482974
No related branches found
No related tags found
No related merge requests found
Bluetooth ECG Service
========================
.. warning::
The service is still work in progress and subject to change
The ECG service provides access to the ECG sensor of the card10
BLE Service
-----------
The current draft uses following service specification:
- Service:
UUID: ``42230300-2342-2342-2342-234223422342``
- ECG samples characteristic:
UUID: ``42230301-2342-2342-2342-234223422342``
notify
ECG samples characteristic
--------------------------
List of 16 bit samples (big endian). Enable notifications to
receive a stream of samples while the ECG app is open.
The first 16 bit are a sample counter (big endian).
......@@ -75,6 +75,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
bluetooth/ess
bluetooth/file-transfer
bluetooth/card10
bluetooth/ecg
bluetooth/nimble
Indices and tables
......
......@@ -97,6 +97,10 @@ void ble_epic_ble_api_trigger_event(enum epic_ble_event_type type, void *data)
int epic_ble_get_event(struct epic_ble_event *e)
{
if (!ble_is_enabled()) {
return -EIO;
}
if (xQueueReceive(ble_event_queue, e, 0) != pdTRUE) {
return -ENOENT;
}
......@@ -162,6 +166,10 @@ void vDmTimerCallback()
int epic_ble_init(void)
{
if (!ble_is_enabled()) {
return -EIO;
}
if (dm_timer == NULL) {
dm_timer = xTimerCreateStatic(
"dmtimer",
......
This diff is collapsed.
......@@ -95,6 +95,7 @@ def ecg_settings():
config.add_option(("Filter", itertools.cycle([("HP", {"HP"}), ("off", {})])))
config.add_option(("Rate", itertools.cycle([("128Hz", 128), ("256Hz", 256)])))
config.add_option(("Window", itertools.cycle([("1x", 1), ("2x", 2), ("3x", 3)])))
config.add_option(("BLE Disp", itertools.cycle([("Off", False), ("On", True)])))
config.add_option(
(
"Log",
......
......@@ -474,9 +474,14 @@ int mp_bluetooth_init(void)
MP_ROM_INT(EPIC_INT_BLE), (mp_obj_t *)&ble_event_obj
);
clear_events();
epic_ble_init();
int ret = epic_ble_init();
if (ret == 0) {
active = true;
return 0;
} else {
active = false;
}
return ret;
}
// Disables the Bluetooth stack. Is a no-op when not enabled.
......
#!/usr/bin/env python3
import bluepy
import time
import struct
import argparse
import numpy as np
import threading
from datetime import datetime
import matplotlib
matplotlib.use("GTK3Agg")
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from scipy import signal
# Set this to true to invert the polarity of the data
invert = True
# Default sample rate of the ECG app
sample_rate = 128
# Larger FFT length gives more BPM resoultion, but takes longer to settle
fft_len = 1024
# Low/High section of the FFT which is shown. In BPM.
fft_low_freq = 40
fft_high_freq = 200
# If you want to see 50/60 Hz line noise in the FFT
# fft_high_freq = 60*60
fft_low_bin = int(fft_low_freq / 60 / (sample_rate / fft_len))
fft_high_bin = int(fft_high_freq / 60 / (sample_rate / fft_len))
# 50 Hz notch filter
b, a = signal.iirnotch(50, 50, sample_rate)
ecg_data = []
# Create figure for plotting
fig, ax = plt.subplots(2, 2, sharey=False)
fig.suptitle("card10 BLE ECG Streaming")
fig.canvas.manager.set_window_title("card10 BLE ECG Streaming")
# This function is called periodically from FuncAnimation
def animate(i):
global ecg_data
# Limit displayed data
xs = np.array(range(len(ecg_data))[-fft_len:]) / sample_rate
ys = np.array(ecg_data[-fft_len:])
if len(ys) == 0:
return
if invert:
ys *= -1
ys_filt = signal.filtfilt(b, a, ys)
# Draw x and y lists
ax[0, 0].clear()
ax[0, 0].plot(xs, ys)
ax[1, 0].clear()
ax[1, 0].plot(xs, ys_filt)
if len(ys) == fft_len:
ax[0, 1].clear()
fft = abs(np.fft.fft(ys)[fft_low_bin:fft_high_bin])
ax[0, 1].plot(
np.linspace(fft_low_freq, fft_high_freq, fft_high_bin - fft_low_bin), fft
)
am = np.argmax(fft) + fft_low_bin
print("BPM:", am * sample_rate / fft_len * 60)
ax[1, 1].clear()
fft = abs(np.fft.fft(ys)[fft_low_bin:fft_high_bin])
ax[1, 1].plot(
np.linspace(fft_low_freq, fft_high_freq, fft_high_bin - fft_low_bin), fft
)
am = np.argmax(fft) + fft_low_bin
print("BPM(filt):", am * sample_rate / fft_len * 60)
# Format plot
# Needs to be here as the clear() call above also clears parts of the formating
ax[0, 0].set_title("ECG Data")
ax[0, 0].set_ylabel("Voltage")
ax[0, 0].set_xlabel("Time [s]")
ax[1, 0].set_title("ECG Data (50 Hz filtered)")
ax[1, 0].set_ylabel("Voltage")
ax[1, 0].set_xlabel("Time [s]")
ax[0, 1].set_title("FFT of ECG Data")
ax[0, 1].set_xlabel("Frequency [BPM]")
ax[1, 1].set_title("FFT of ECG Data (50 Hz filtered)")
ax[1, 1].set_xlabel("Frequency [BPM]")
def plot_thread():
# Set up plot to call animate() function periodically
ani = animation.FuncAnimation(fig, animate, interval=500)
plt.show()
def main() -> None:
parser = argparse.ArgumentParser(
description="""\
Transfer sensor data using Bluetooth Low Energy.
"""
)
parser.add_argument(
"mac", help="BT MAC address of the card10. Format: CA:4D:10:XX:XX:XX"
)
args = parser.parse_args()
t0 = time.time()
p = bluepy.btle.Peripheral(args.mac)
# We need a larger MTU than the default MTU
p.setMTU(90)
c = p.getCharacteristics(uuid="42230301-2342-2342-2342-234223422342")[0]
# Enable streaming
c.getDescriptors()[0].write(b"\x01\x00")
print("Connection setup time:", int(time.time() - t0), "seconds")
class ECGDelegate(bluepy.btle.DefaultDelegate):
def __init__(self):
bluepy.btle.DefaultDelegate.__init__(self)
self.t0 = time.time()
def handleNotification(self, cHandle, data):
global ecg_data
if cHandle == c.valHandle:
index, *volts = struct.unpack(
">H" + ("h" * ((len(data) // 2) - 1)), data
)
t = time.time()
dt = t - self.t0
self.t0 = t
print(f"{dt:.3f}", cHandle, index, volts)
ecg_data += volts
p.setDelegate(ECGDelegate())
pt = threading.Thread(target=plot_thread)
pt.daemon = True
pt.start()
while pt.is_alive():
# All the magic happens in the delegate and the plotting thread.
# We just spin in here and tell bluepy that we want to receive
# notification.
p.waitForNotifications(1.0)
if __name__ == "__main__":
main()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment