cck33
Published © MIT

Heart Rate Variability (HRV) and Biohacking the Bangle.js 2

Transform your Bangle. js into a biohacking tool with HRV apps - track and train your heart for resilience, all without costly gadgets!

BeginnerProtip1 hour175
Heart Rate Variability (HRV) and Biohacking the Bangle.js 2

Things used in this project

Hardware components

Espruino Bangle.js 2
×1

Software apps and online services

Espruino IDE

Story

Read more

Schematics

logofile

you also need to upload this one to have a logo in the menu for the apps. rename it to hrvapp.img.

Code

Upload the hrv-training.app.js

JavaScript
upload to bangle.js 2 smartwatch via espruino ide
// Initialisierung
var startTime = new Date().getTime();
var bpmValues = []; // Speichert BPM-Werte für die Durchschnittsberechnung
var rrIntervals = []; // Speichert RR-Intervalle für die HRV-Berechnung
var breathCycle = 0; // Zähler für den Atemzyklus

// UI- und Anzeigefunktionen
function initUI() {
  g.clear();
  Bangle.loadWidgets();
  Bangle.drawWidgets();
  g.setColor(g.theme.fg);
}

function updateDisplay() {
  var avgBpm = bpmValues.length > 0 ? bpmValues.reduce((acc, val) => acc + val, 0) / bpmValues.length : "--";
  var hrv = calcHrv(); // Berechne den HRV-Wert

  g.clear();
  Bangle.drawWidgets();
  g.setFontAlign(0, 0);
  g.setFont("Vector", 20);

  // Zeichne die Uhrzeit am oberen Rand des Displays
  var currentTime = new Date();
  var hours = currentTime.getHours();
  var minutes = currentTime.getMinutes();
  var timeString = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2);
  g.drawString(timeString, 55, 14); // Anpassung der x-Koordinate

  // Zeichne BPM und HRV über dem Graphen
  if (!isNaN(avgBpm)) {
    g.drawString(`BPM: ${avgBpm.toFixed(2)}`, g.getWidth() / 2, 50); // BPM mit maximal 2 Dezimalstellen
  } else {
    g.drawString("BPM: --", g.getWidth() / 2, 50); // Wenn avgBpm keine Zahl ist, zeige "--" an
  }
  g.drawString(`HRV: ${hrv.toFixed(2)} ms`, g.getWidth() / 2, 70); // HRV mit maximal 2 Dezimalstellen
  
  // Zeichne den BPM-Graphen
  drawGraph(bpmValues);
}


// HRV-Berechnungsfunktion
function calcHrv() {
  if (bpmValues.length < 2) return 0; // Keine Berechnung, wenn unzureichende Daten vorhanden sind
  var initialBpm = bpmValues[0];
  var totalDeviation = 0;

  for (var bpm of bpmValues) {
    totalDeviation += Math.abs(bpm - initialBpm);
  }

  var coefficient = totalDeviation / (bpmValues.length - 1);
  return coefficient;
}

// Variable für die BPM-Graph-Optionen
var graphOptions = {
  x: 0,
  y: 90,
  width: g.getWidth(),
  height: g.getHeight() - 90,
  minValue: 0,
  maxValue: 200, // Maximale BPM-Werte
  gridColor: g.theme.bg, // Hintergrundfarbe für den Graphen
  color: g.theme.fg, // Farbe der BPM-Linie
  grid: true, // Gitterlinien aktivieren
  title: "Heart Rate Graph" // Titel des Graphen angepasst
};

// Funktion zum Zeichnen des Graphen
function drawGraph(data) {
  var avgBpm = data.reduce((acc, val) => acc + val, 0) / data.length;
  var range = 5; // Zusätzlicher Bereich um den durchschnittlichen BPM-Wert

  var maxY = Math.max.apply(null, data.concat([avgBpm + range]));
  var minY = Math.min.apply(null, data.concat([avgBpm - range]));

  // Zeichne den Hintergrund
  g.clearRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);
  g.setColor(graphOptions.gridColor);
  g.fillRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die vertikalen Gitterlinien
  if (graphOptions.grid) {
    g.setColor(g.theme.hl);
    for (var i = 1; i < 10; i++) {
      var y = graphOptions.y + (i * graphOptions.height / 10);
      g.drawLine(graphOptions.x, y, graphOptions.x + graphOptions.width, y);
    }
  }

  // Zeichne die horizontale Gitterlinie bei 0
  g.setColor(g.theme.hl);
  g.drawLine(graphOptions.x, graphOptions.y + graphOptions.height, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die BPM-Linie
  g.setColor(graphOptions.color);
  for (var i = 1; i < data.length; i++) {
    var y0 = graphOptions.y + graphOptions.height - ((data[i - 1] - minY) / (maxY - minY)) * graphOptions.height;
    var y1 = graphOptions.y + graphOptions.height - ((data[i] - minY) / (maxY - minY)) * graphOptions.height;
    g.drawLine(graphOptions.x + (i - 1) * (graphOptions.width / (data.length - 1)), y0, graphOptions.x + i * (graphOptions.width / (data.length - 1)), y1);
  }

  // Zeichne die Titel
  g.setColor(graphOptions.color);
  g.setFont("6x8", 1); // Kleinere Schriftgröße für den Titel
  g.setFontAlign(0, -1);
  g.drawString(graphOptions.title, graphOptions.x + graphOptions.width / 2, graphOptions.y + 5);
}

// Ereignisbehandlung für den HRM
Bangle.on('HRM', function(e) {
  if (e.bpm > 0) {
    bpmValues.push(e.bpm); // Speichere den aktuellen BPM-Wert
    if (bpmValues.length > 30) bpmValues.shift(); // Begrenze die Anzahl der gespeicherten BPM-Werte

    // Starte die Atemführung, wenn BPM-Daten verfügbar sind
    if (breathCycle % 7 === 0) {
      if (bpmValues.length >= 7) {
        Bangle.buzz(33); // Kurze Vibration
        breathCycle = 0; // Setze den Atemzyklus-Zähler zurück
      }
    }

    breathCycle++; // Zähle den Atemzyklus
  }

  if (e.rr) {
    rrIntervals = rrIntervals.concat(e.rr.map(rr => rr / 1000)); // Konvertiere RR-Werte von ms zu Sekunden
    if (rrIntervals.length > 100) rrIntervals.shift(); // Begrenze die Anzahl der gespeicherten RR-Intervalle
  }

  updateDisplay(); // Aktualisiere die Anzeige mit den neuesten Werten
});

// Beim Beenden die HRM abschalten
E.on('kill', function() {
  Bangle.setHRMPower(0);
});

// App-Initialisierung
function init() {
  initUI();
  Bangle.setHRMPower(1, "app"); // Starte die HRM-Datenerfassung
}

init();

Upload the hrvapp.app.js

JavaScript
upload to the bangle.js 2 smartwatch via espruino ide
// Initialisierung
var startTime = new Date().getTime();
var bpmValues = []; // Speichert BPM-Werte für die Durchschnittsberechnung
var rrIntervals = []; // Speichert RR-Intervalle für die HRV-Berechnung

// UI- und Anzeigefunktionen
function initUI() {
  g.clear();
  Bangle.loadWidgets();
  Bangle.drawWidgets();
  g.setColor(g.theme.fg);
}

function updateDisplay() {
  var avgBpm = bpmValues.length > 0 ? bpmValues.reduce((acc, val) => acc + val, 0) / bpmValues.length : "--";
  var hrv = calcHrv(); // Berechne den HRV-Wert

  g.clear();
  Bangle.drawWidgets();
  g.setFontAlign(0, 0);
  g.setFont("Vector", 20);

  // Zeichne die Uhrzeit am oberen Rand des Displays
  var currentTime = new Date();
  var hours = currentTime.getHours();
  var minutes = currentTime.getMinutes();
  var timeString = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2);
  g.drawString(timeString, 55, 14); // Anpassung der x-Koordinate

  // Zeichne BPM und HRV über dem Graphen
  if (!isNaN(avgBpm)) {
    g.drawString(`BPM: ${avgBpm.toFixed(2)}`, g.getWidth() / 2, 50); // BPM mit maximal 2 Dezimalstellen
  } else {
    g.drawString("BPM: --", g.getWidth() / 2, 50); // Wenn avgBpm keine Zahl ist, zeige "--" an
  }
  g.drawString(`HRV: ${hrv.toFixed(2)} ms`, g.getWidth() / 2, 70); // HRV mit maximal 2 Dezimalstellen
  
  // Zeichne den BPM-Graphen
  drawGraph(bpmValues);
}



// HRV-Berechnungsfunktion
function calcHrv() {
  if (bpmValues.length < 2) return 0; // Keine Berechnung, wenn unzureichende Daten vorhanden sind
  var initialBpm = bpmValues[0];
  var totalDeviation = 0;

  for (var bpm of bpmValues) {
    totalDeviation += Math.abs(bpm - initialBpm);
  }

  var coefficient = totalDeviation / (bpmValues.length - 1);
  return coefficient;
}

// Variable für die BPM-Graph-Optionen
var graphOptions = {
  x: 0,
  y: 90,
  width: g.getWidth(),
  height: g.getHeight() - 90,
  minValue: 0,
  maxValue: 200, // Maximale BPM-Werte
  gridColor: g.theme.bg, // Hintergrundfarbe für den Graphen
  color: g.theme.fg, // Farbe der BPM-Linie
  grid: true, // Gitterlinien aktivieren
  title: "Heart Rate Graph" // Titel des Graphen angepasst
};

// Funktion zum Zeichnen des Graphen
function drawGraph(data) {
  var avgBpm = data.reduce((acc, val) => acc + val, 0) / data.length;
  var range = 5; // Zusätzlicher Bereich um den durchschnittlichen BPM-Wert

  var maxY = Math.max.apply(null, data.concat([avgBpm + range]));
  var minY = Math.min.apply(null, data.concat([avgBpm - range]));

  // Zeichne den Hintergrund
  g.clearRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);
  g.setColor(graphOptions.gridColor);
  g.fillRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die vertikalen Gitterlinien
  if (graphOptions.grid) {
    g.setColor(g.theme.hl);
    for (var i = 1; i < 10; i++) {
      var y = graphOptions.y + (i * graphOptions.height / 10);
      g.drawLine(graphOptions.x, y, graphOptions.x + graphOptions.width, y);
    }
  }

  // Zeichne die horizontale Gitterlinie bei 0
  g.setColor(g.theme.hl);
  g.drawLine(graphOptions.x, graphOptions.y + graphOptions.height, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die BPM-Linie
  g.setColor(graphOptions.color);
  for (var i = 1; i < data.length; i++) {
    var y0 = graphOptions.y + graphOptions.height - ((data[i - 1] - minY) / (maxY - minY)) * graphOptions.height;
    var y1 = graphOptions.y + graphOptions.height - ((data[i] - minY) / (maxY - minY)) * graphOptions.height;
    g.drawLine(graphOptions.x + (i - 1) * (graphOptions.width / (data.length - 1)), y0, graphOptions.x + i * (graphOptions.width / (data.length - 1)), y1);
  }

  // Zeichne die Titel
  g.setColor(graphOptions.color);
  g.setFont("6x8", 1); // Kleinere Schriftgröße für den Titel
  g.setFontAlign(0, -1);
  g.drawString(graphOptions.title, graphOptions.x + graphOptions.width / 2, graphOptions.y + 5);
}


// Ereignisbehandlung für den HRM
Bangle.on('HRM', function(e) {
  if (e.bpm > 0) {
    bpmValues.push(e.bpm); // Speichere den aktuellen BPM-Wert
    if (bpmValues.length > 30) bpmValues.shift(); // Begrenze die Anzahl der gespeicherten BPM-Werte
  }

  if (e.rr) {
    rrIntervals = rrIntervals.concat(e.rr.map(rr => rr / 1000)); // Konvertiere RR-Werte von ms zu Sekunden
    if (rrIntervals.length > 100) rrIntervals.shift(); // Begrenze die Anzahl der gespeicherten RR-Intervalle
  }

  updateDisplay(); // Aktualisiere die Anzeige mit den neuesten Werten
});

// Beim Beenden die HRM abschalten
E.on('kill', function() {
  Bangle.setHRMPower(0);
});

// App-Initialisierung
function init() {
  initUI();
  Bangle.setHRMPower(1, "app"); // Starte die HRM-Datenerfassung
}

init();

Run to RAM to Install the "HRV Guide" app

JavaScript
copy to the espruino ide after uploading the .app.js files
{"id":"hrv","name":"HRV Guide","src":"hrv-training.app.js","icon":"hrvapp.img","type":"app"}

Run to RAM to Install the "HRV" app

JavaScript
copy to the espruino ide after uploading the other files and run it to the smartwatch to install.
{"id":"hrvapp","name":"HRV","src":"hrvapp.app.js","icon":"hrvapp.img","type":"app"}

HRV Log app

JavaScript
hrv-data-0.log, hrv-data-1.log, hrv-data-2.log, ... - these files will be creates. you need to merge and visualize them to see your session
// Initialisierung
var startTime = new Date().getTime();
var bpmValues = []; // Speichert BPM-Werte für die Durchschnittsberechnung
var rrIntervals = []; // Speichert RR-Intervalle für die HRV-Berechnung
var breathCycle = 0; // Zähler für den Atemzyklus
var isLogging = false; // Zustand des Loggings
var logIndex = 0; // Globale Variable zur Speicherung des Log-Indexes

// UI- und Anzeigefunktionen
function initUI() {
  g.clear();
  Bangle.loadWidgets();
  Bangle.drawWidgets();
  g.setColor(g.theme.fg);
}

function updateDisplay() {
  var avgBpm = bpmValues.length > 0 ? bpmValues.reduce((acc, val) => acc + val, 0) / bpmValues.length : "--";
  var hrv = calcHrv(); // Berechne den HRV-Wert

  g.clear();
  Bangle.drawWidgets();
  g.setFontAlign(0, 0);
  g.setFont("Vector", 20);

  // Zeichne die Uhrzeit am oberen Rand des Displays
  var currentTime = new Date();
  var hours = currentTime.getHours();
  var minutes = currentTime.getMinutes();
  var timeString = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2);
  g.drawString(timeString, g.getWidth() / 2, 10); // Zentriere die Uhrzeit

  // Zeichne den Button für das Logging
  g.setColor(isLogging ? "#00FF00" : "#FF0000"); // Grün, wenn logging aktiv, sonst Rot
  g.fillRect(0, 23, g.getWidth(), 43); // Zeichne den Button über die gesamte Bildschirmbreite
  g.setColor("#000000"); // Textfarbe Schwarz
  g.setFont("Vector", 18);
  g.drawString(isLogging ? "Stop" : "Start", g.getWidth() / 2, 33);

  // Zeichne BPM und HRV über dem Graphen
  g.setColor(g.theme.fg);
  if (!isNaN(avgBpm)) {
    g.drawString(`BPM: ${avgBpm.toFixed(2)}`, g.getWidth() / 2, 55); // BPM mit maximal 2 Dezimalstellen
  } else {
    g.drawString("BPM: --", g.getWidth() / 2, 55); // Wenn avgBpm keine Zahl ist, zeige "--" an
  }
  g.drawString(`HRV: ${hrv.toFixed(2)} ms`, g.getWidth() / 2, 73); // HRV mit maximal 2 Dezimalstellen
  
  // Weiterer UI-Code für den Graphen usw.
  drawGraph(bpmValues);
}

// Touch-Handling für den Button
Bangle.on('touch', function(button, xy) {
  if (xy.y >= 23 && xy.y <= 43) {
    toggleLogging();
    updateDisplay();
  }
});

// Funktion zum Starten/Stoppen des Loggings
function toggleLogging() {
  isLogging = !isLogging; // Toggle-Zustand
  if (isLogging) {
    console.log("Start Log");
    Bangle.buzz(100); // Feedback, dass das Logging gestartet wurde
    // Leere die Arrays, wenn das Logging gestartet wird
    bpmValues = [];
    rrIntervals = [];
  } else {
    console.log("Stop Log");
    Bangle.buzz(100); // Feedback, dass das Logging gestoppt wurde
    saveLog(); // Speichere die gesammelten Daten, wenn das Logging gestoppt wird
  }
}

// Funktion zum Speichern der Log-Daten im externen Speicher
function saveLog() {
  var currentTime = new Date().getTime();
  var logData = {
    startTime: startTime,
    endTime: currentTime,
    duration: currentTime - startTime,
    bpmValues: bpmValues, // Verwenden Sie die originalen BPM-Werte ohne Rundung
    rrIntervals: rrIntervals
  };
  var logString = JSON.stringify(logData);
  var fileName = "hrv-data-" + logIndex + ".log";
  
  // Prüfen, ob der externe Speicher verfügbar ist
  if (require("Storage").getFree() > logString.length) {
    require("Storage").write(fileName, logString, logIndex === 0 ? "w" : "a");
    console.log("Log gespeichert in: " + fileName);
    logIndex++;
  } else {
    console.log("Nicht genügend Speicherplatz verfügbar.");
  }
  
  // Bereite für das nächste Logging vor
  startTime = currentTime;
  bpmValues = [];
  rrIntervals = [];
}


// Funktion zum Zusammenführen der Log-Daten aus allen Dateien
function mergeLogData() {
  var mergedData = {
    startTime: null,
    endTime: null,
    bpmValues: [],
    rrIntervals: []
  };

  var logFiles = require("Storage").list(/hrv-data-\d+\.log/);
  var totalFiles = logFiles.length;
  var currentFile = 0;

  logFiles.forEach(function(filename) {
    var logData = require("Storage").readJSON(filename);
    if (logData) {
      if (!mergedData.startTime) {
        mergedData.startTime = logData.startTime;
      }
      mergedData.endTime = logData.endTime;
      mergedData.bpmValues = mergedData.bpmValues.concat(logData.bpmValues);
      mergedData.rrIntervals = mergedData.rrIntervals.concat(logData.rrIntervals);
    }

    // Aktualisiere den Fortschrittsbalken
    currentFile++;
    var progress = (currentFile / totalFiles) * g.getWidth();
    g.clearRect(0, 140, g.getWidth(), 160); // Lösche vorherigen Fortschrittsbalken
    g.fillRect(0, 140, progress, 160); // Zeichne neuen Fortschrittsbalken
    g.flip(); // Aktualisiere das Display
  });
  
  return mergedData;
}

// Ereignisbehandlung für den HRM
Bangle.on('HRM', function(e) {
  if (isLogging) {
    if (e.bpm > 0) {
      bpmValues.push(e.bpm);
      // Begrenze die Anzahl der gespeicherten BPM-Werte und speichere frühzeitig, falls notwendig
      if (bpmValues.length >= 30) {
        saveLog(); // Speichere die aktuellen Daten und beginne ein neues Log
      }
    }

    if (e.rr) {
      rrIntervals = rrIntervals.concat(e.rr.map(rr => rr / 1000));
      // Optional: Begrenze auch die RR-Intervalle und speichere frühzeitig, falls notwendig
    }
  }
});

// App-Initialisierung
function init() {
  initUI();
  Bangle.setHRMPower(1, "app");
  // Setze logIndex basierend auf existierenden Dateien, um Überschreibungen zu vermeiden
  var existingLogs = require("Storage").list(/hrv-data-\d+\.log/);
  logIndex = existingLogs.length;
}

// HRV-Berechnungsfunktion
function calcHrv() {
  if (bpmValues.length < 2) return 0; // Keine Berechnung, wenn unzureichende Daten vorhanden sind
  var initialBpm = bpmValues[0];
  var totalDeviation = 0;

  for (var bpm of bpmValues) {
    totalDeviation += Math.abs(bpm - initialBpm);
  }

  var coefficient = totalDeviation / (bpmValues.length - 1);
  return coefficient;
}

// Variable für die BPM-Graph-Optionen
var graphOptions = {
  x: 0,
  y: 90,
  width: g.getWidth(),
  height: g.getHeight() - 90,
  minValue: 0,
  maxValue: 200, // Maximale BPM-Werte
  gridColor: g.theme.bg, // Hintergrundfarbe für den Graphen
  color: g.theme.fg, // Farbe der BPM-Linie
  grid: true, // Gitterlinien aktivieren
  title: "Heart Rate Graph" // Titel des Graphen angepasst
};

// Funktion zum Zeichnen des Graphen
function drawGraph(data) {
  var avgBpm = data.reduce((acc, val) => acc + val, 0) / data.length;
  var range = 5; // Zusätzlicher Bereich um den durchschnittlichen BPM-Wert

  var maxY = Math.max.apply(null, data.concat([avgBpm + range]));
  var minY = Math.min.apply(null, data.concat([avgBpm - range]));

  // Zeichne den Hintergrund
  g.clearRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);
  g.setColor(graphOptions.gridColor);
  g.fillRect(graphOptions.x, graphOptions.y, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die vertikalen Gitterlinien
  if (graphOptions.grid) {
    g.setColor(g.theme.hl);
    for (var i = 1; i < 10; i++) {
      var y = graphOptions.y + (i * graphOptions.height / 10);
      g.drawLine(graphOptions.x, y, graphOptions.x + graphOptions.width, y);
    }
  }

  // Zeichne die horizontale Gitterlinie bei 0
  g.setColor(g.theme.hl);
  g.drawLine(graphOptions.x, graphOptions.y + graphOptions.height, graphOptions.x + graphOptions.width, graphOptions.y + graphOptions.height);

  // Zeichne die BPM-Linie
  g.setColor(graphOptions.color);
  for (var i = 1; i < data.length; i++) {
    var y0 = graphOptions.y + graphOptions.height - ((data[i - 1] - minY) / (maxY - minY)) * graphOptions.height;
    var y1 = graphOptions.y + graphOptions.height - ((data[i] - minY) / (maxY - minY)) * graphOptions.height;
    g.drawLine(graphOptions.x + (i - 1) * (graphOptions.width / (data.length - 1)), y0, graphOptions.x + i * (graphOptions.width / (data.length - 1)), y1);
  }

  // Zeichne die Titel
  g.setColor(graphOptions.color);
  g.setFont("6x8", 1); // Kleinere Schriftgröße für den Titel
  g.setFontAlign(0, -1);
  g.drawString(graphOptions.title, graphOptions.x + graphOptions.width / 2, graphOptions.y + 5);
}

// Ereignisbehandlung für den HRM
Bangle.on('HRM', function(e) {
  if (e.bpm > 0 && isLogging) {
    bpmValues.push(e.bpm); // Speichere den aktuellen BPM-Wert
    if (bpmValues.length > 30) bpmValues.shift(); // Begrenze die Anzahl der gespeicherten BPM-Werte
  }

  if (e.rr && isLogging) {
    rrIntervals = rrIntervals.concat(e.rr.map(rr => rr / 1000)); // Konvertiere RR-Werte von ms zu Sekunden
    if (rrIntervals.length > 100) rrIntervals.shift(); // Begrenze die Anzahl der gespeicherten RR-Intervalle
  }

  updateDisplay(); // Aktualisiere die Anzeige mit den neuesten Werten
});

// Beim Beenden die HRM abschalten
E.on('kill', function() {
  Bangle.setHRMPower(0);
});

// App-Initialisierung
function init() {
  initUI();
  Bangle.setHRMPower(1, "app"); // Starte die HRM-Datenerfassung
}

init();

hrv-log.info

JavaScript
execute this to the ram of the espruino to install the uploaded .app.js file
require("Storage").write("hrv-log.info", {
  "id": "hrv-log",
  "name": "HRV Log",
  "src": "hrv-log.app.js",
  "icon": "hrvapp.img",
  "type": "app"
});

Credits

cck33

cck33

3 projects • 0 followers

Comments