EdOliverAlejandro Sanchez
Created June 15, 2021 © MIT

EHM: Electrocardiography Holter Monitor

Electrocardiogram Holter monitor with heart rate analysis and dashboard. Cloud based and powered by Nordic Semiconductors.

AdvancedFull instructions provided16 hours768

Things used in this project

Story

Read more

Custom parts and enclosures

nrf case bottom

ECG case bottom

ECG case top

nrf case top

Schematics

System Architecture

The full idea of the system

ECG

ECG schematics

nRF

nRF schematics

Code

Power profiler RasPI code

Python
For the RPi + PPKII combo. This is just an example of the code used, please go to our github to see it all its very extensive.
import time
import csv
import datetime
from threading import Thread
# import numpy as np
# import matplotlib.pyplot as plt
# import matplotlib
from src.ppk2_api import PPK2_API

class PowerProfiler():
    def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None):
        """Initialize PPK2 power profiler with serial"""
        self.measuring = None
        self.measurement_thread = None
        self.ppk2 = None

        try:
            if serial_port:
                self.ppk2 = PPK2_API(serial_port)
            else:
                serial_port = self.discover_port()
                if serial_port:
                    self.ppk2 = PPK2_API(serial_port)
            ret = self.ppk2.get_modifiers()  # try to read modifiers, if it fails serial port is probably not correct
        except Exception as e:
            ret = None
            raise e

        if not ret:
            self.ppk2 = None
            #raise Exception(f"Error when initing PowerProfiler with serial port {serial_port}")
        else:
            self.ppk2.use_source_meter()

            self.source_voltage_mV = source_voltage_mV

            self.ppk2.set_source_voltage(self.source_voltage_mV)  # set to 3.3V

            self.measuring = False
            self.current_measurements = []

            # local variables used to calculate power consumption
            self.measurement_start_time = None
            self.measurement_stop_time = None

            time.sleep(1)

            self.stop = False

            self.measurement_thread = Thread(target=self.measurement_loop, daemon=True)
            self.measurement_thread.start()

            # write to csv
            self.filename = filename
            if self.filename is not None:
                with open(self.filename, 'w', newline='') as file:
                    writer = csv.writer(file)
                    row = []
                    for key in ["ts", "avg1000"]:
                        row.append(key)
                    writer.writerow(row)

    def write_csv_rows(self, samples):
        """Write csv row"""
        with open(self.filename, 'a', newline='') as file:
            writer = csv.writer(file)
            for sample in samples:
                row = [datetime.datetime.now().strftime('%d-%m-%Y %H:%M:%S.%f'), sample]
                writer.writerow(row)

    def delete_power_profiler(self):
        """Join thread"""
        self.measuring = False
        self.stop = True

        if self.measurement_thread:
            self.measurement_thread.join()
            self.measurement_thread = None

        if self.ppk2:
            self.disable_power()

    def discover_port(self):
        """Discovers ppk2 serial port"""
        ppk2s_connected = PPK2_API.list_devices()
        if(len(ppk2s_connected) == 1):
            ppk2_port = ppk2s_connected[0]
            print(f'Found PPK2 at {ppk2_port}')
            return ppk2_port
        else:
            print(f'Too many connected PPK2\'s: {ppk2s_connected}')
            return None

    def enable_power(self):
        """Enable ppk2 power"""
        if self.ppk2:
            self.ppk2.toggle_DUT_power("ON")
            return True
        return False

    def disable_power(self):
        """Disable ppk2 power"""
        if self.ppk2:
            self.ppk2.toggle_DUT_power("OFF")
            return True
        return False

    def measurement_loop(self):
        """Endless measurement loop will run in a thread"""
        while True and not self.stop:
            if self.measuring:  # read data if currently measuring
                read_data = self.ppk2.get_data()
                if read_data != b'':
                    #samples = self.ppk2.get_samples(read_data)
                    samples = self._average_samples(self.ppk2.get_samples(read_data), 1024)  # optionally average samples
                    self.current_measurements += samples  # can easily sum lists, will append individual data
            time.sleep(0.001)  # TODO figure out correct sleep duration

    def _average_samples(self, list, window_size):
        """Average samples based on window size"""
        chunks = [list[val:val + window_size] for val in range(0, len(list), window_size)]
        avgs = []
        for chunk in chunks:
            avgs.append(sum(chunk) / len(chunk))

        return avgs

    def start_measuring(self):
        """Start measuring"""
        if not self.measuring:  # toggle measuring flag only if currently not measuring
            self.current_measurements = []  # reset current measurements
            self.ppk2.start_measuring()  # send command to ppk2
            self.measuring = True  # set internal flag
            self.measurement_start_time = time.time()

    def stop_measuring(self):
        """Stop measuring and return average of period"""
        self.measurement_stop_time = time.time()
        self.measuring = False
        self.ppk2.stop_measuring()  # send command to ppk2

        #samples_average = self._average_samples(self.current_measurements, 1000)
        if self.filename is not None:
            self.write_csv_rows(self.current_measurements)

    def get_min_current_mA(self):
        return min(self.current_measurements) / 1000

    def get_max_current_mA(self):
        return max(self.current_measurements) / 1000

    def get_average_current_mA(self):
        """Returns average current of last measurement in mA"""
        if len(self.current_measurements) == 0:
            return 0

        average_current_mA = (sum(self.current_measurements) / len(self.current_measurements)) / 1000 # measurements are in microamperes, divide by 1000
        return average_current_mA

    def get_average_power_consumption_mWh(self):
        """Return average power consumption of last measurement in mWh"""
        average_current_mA = self.get_average_current_mA()  # convert microamperes to milliamperes
        average_power_mW = (self.source_voltage_mV / 1000) * average_current_mA  # divide by 1000 as source voltage is in millivolts - this gives us milliwatts
        measurement_duration_h = self.get_measurement_duration_s() / 3600  # duration in seconds, divide by 3600 to get hours
        average_consumption_mWh = average_power_mW * measurement_duration_h
        return average_consumption_mWh

    def get_average_charge_mC(self):
        """Returns average charge in milli coulomb"""
        average_current_mA = self.get_average_current_mA()
        measurement_duration_s = self.get_measurement_duration_s()  # in seconds
        return average_current_mA * measurement_duration_s

    def get_measurement_duration_s(self):
        """Returns duration of measurement"""
        measurement_duration_s = (self.measurement_stop_time - self.measurement_start_time)  # measurement duration in seconds
        return measurement_duration_s

# pp = PowerProfiler("/dev/ttyACM1")
# pp.start_measuring()
# time.sleep(10)
# pp.stop_measuring()

Node red Flow

JavaScript
The flow for the Node RED used.
[{"id":"a9631253.e2dca","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"bb645c7.1a3fca","type":"function","z":"a9631253.e2dca","name":"","func":"let p=JSON.parse(msg.payload);\np = p[\"message\"][\"event\"][\"characteristic\"]\np = { payload: Math.round(((parseInt(p[\"value\"][2])*1.8+32) + Number.EPSILON) * 100) / 100};\nreturn p;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":140,"wires":[["d836c20f.e5c8f"]]},{"id":"cdc13c40.4b35f","type":"mqtt in","z":"a9631253.e2dca","name":"MQTT","topic":"prod/XXXXXXXXXXXXXXXXXX1/a/gateways","qos":"0","datatype":"auto","broker":"40073673.269718","nl":false,"rap":false,"x":250,"y":200,"wires":[["bb645c7.1a3fca","199adf51.c62471","febd5bc3.cf78f8"]]},{"id":"199adf51.c62471","type":"function","z":"a9631253.e2dca","name":"","func":"let p=JSON.parse(msg.payload);\np = p[\"message\"][\"event\"][\"characteristic\"]\np = { payload: p[\"value\"][1]};\nreturn p;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":260,"wires":[["4f290812.2d5198"]]},{"id":"d836c20f.e5c8f","type":"ui_gauge","z":"a9631253.e2dca","name":"","group":"45bd7d24.688564","order":0,"width":"0","height":"0","gtype":"gage","title":"Temperature","label":"°F","format":"{{value}}","min":"90","max":"109","colors":["#003eb3","#11ff00","#ff0000"],"seg1":"97","seg2":"100.4","x":670,"y":140,"wires":[]},{"id":"6006f6b.6b2f308","type":"mqtt in","z":"a9631253.e2dca","name":"","topic":"/station/1/hr","qos":"2","datatype":"auto","broker":"15df0989.342bf6","nl":false,"rap":true,"rh":0,"x":270,"y":380,"wires":[["a485905d.3834e"]]},{"id":"a485905d.3834e","type":"function","z":"a9631253.e2dca","name":"","func":"let p=msg.payload;\nlet data = p.split(\" \")\n\nvar obj = [{\n    \"series\":['A'],\n    \"data\":[data],\n    \"labels\": Array.from(Array(data.length).keys())\n}]\n\nmsg.payload = obj\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":380,"wires":[["35593be2.f1b0d4"]]},{"id":"35593be2.f1b0d4","type":"ui_chart","z":"a9631253.e2dca","name":"","group":"f62c6eaf.2f8b2","order":2,"width":0,"height":0,"label":"EKG [uA]","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#b31e1e","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":660,"y":380,"wires":[[]]},{"id":"febd5bc3.cf78f8","type":"function","z":"a9631253.e2dca","name":"","func":"let p=JSON.parse(msg.payload);\np = p[\"message\"][\"event\"][\"characteristic\"]\np = { payload: p[\"value\"][0]};\nreturn p;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":200,"wires":[["9808cecc.0db41"]]},{"id":"9808cecc.0db41","type":"ui_gauge","z":"a9631253.e2dca","name":"","group":"45bd7d24.688564","order":0,"width":"0","height":"0","gtype":"gage","title":"SPO2","label":"%","format":"{{value}}","min":"0","max":"100","colors":["#003eb3","#11ff00","#ff0000"],"seg1":"90","seg2":"100","x":650,"y":200,"wires":[]},{"id":"4f290812.2d5198","type":"ui_gauge","z":"a9631253.e2dca","name":"","group":"45bd7d24.688564","order":0,"width":"0","height":"0","gtype":"gage","title":"BPM","label":"bpm","format":"{{value}}","min":"40","max":"240","colors":["#003eb3","#11ff00","#ff0000"],"seg1":"60","seg2":"120","x":650,"y":260,"wires":[]},{"id":"40073673.269718","type":"mqtt-broker","name":"","broker":"XXXXXXXXXXXXXXX.iot.us-east-1.amazonaws.com","port":"8883","tls":"1e103949.ad9827","clientid":"account-XXXXXXXXXXXXXXXXXX1","usetls":true,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"45bd7d24.688564","type":"ui_group","name":"Vital Signs Station","tab":"7c9c8c14.371a84","order":1,"disp":true,"width":"6","collapse":true},{"id":"15df0989.342bf6","type":"mqtt-broker","name":"","broker":"192.168.0.23","port":"1883","clientid":"","usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"f62c6eaf.2f8b2","type":"ui_group","name":"PPKII EKG","tab":"7c9c8c14.371a84","order":2,"disp":true,"width":"6","collapse":false},{"id":"1e103949.ad9827","type":"tls-config","name":"","cert":"","key":"","ca":"","certname":"client.cert","keyname":"priv.cert","caname":"ca.cert","servername":"","verifyservercert":true},{"id":"7c9c8c14.371a84","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

Repository with all the code commented and more

Credits

EdOliver

EdOliver

38 projects • 73 followers
Engineer, Scientist, Maker. Entrepreneur and Futurist.
Alejandro Sanchez

Alejandro Sanchez

9 projects • 19 followers
I am a biomedical engineer working at Boston Scientific as a service engineer

Comments