Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:proiecte:2025sric:runtrack32 [2025/05/28 13:05]
alexandra.cornea01
iothings:proiecte:2025sric:runtrack32 [2025/05/29 02:31] (current)
alexandra.cornea01 [Demonstration and Results]
Line 1: Line 1:
-===== RunTrack32: Smart Running Assistant ​=====+==== RunTrack32: Smart Running Assistant ====
  
   * Author: ​   Alexandra Cornea   * Author: ​   Alexandra Cornea
Line 7: Line 7:
 ==== Introduction ==== ==== Introduction ====
  
 +The RunTracker32 Android application is a real-time health and location monitoring tool designed to interface with a custom Bluetooth-enabled fitness device. The app displays live data such as heart rate (BPM), blood oxygen level (SpO₂), GPS coordinates,​ and movement speed (in km/h), allowing users to track their vitals and position during physical activity. Additionally,​ the app offers quick access to Google Maps for viewing the current location.
 ==== Hardware Design ==== ==== Hardware Design ====
  
 +The system is built around an ESP32 development board, which serves as the central controller. It integrates two key sensors:
 +
 +  * GPS module (Neo-6M) – for tracking geographic coordinates
 +  * Pulse oximeter (MAX30100) – for measuring heart rate and SpO₂ levels
 +
 +The sensors are powered via the breadboard'​s power rails, and communication is established using serial (for GPS) and I2C (for MAX30100).
 +
 +{{:​iothings:​proiecte:​2025sric:​runtracker.jpg?​570|}}
 +
 +**GPS (Neo-6M) Connections**
 +   ​TX ​  ​→ ​ GPIO16 ​ —  Serial RX (ESP32 receives)
 +   ​RX ​  ​→ ​ GPIO17 ​ —  Serial TX (ESP32 sends)
 +   ​VCC ​ →  3.3V    —  Power
 +   ​GND ​ →  GND     ​— ​ Ground
 +
 +**MAX30100 Connections**
 +   ​SDA ​ →  GPIO21 ​ —  I2C Data
 +   ​SCL ​ →  GPIO22 ​ —  I2C Clock
 +   ​VCC ​ →  3.3V    —  Power
 +   ​GND ​ →  GND     ​— ​ Ground
 ==== Software Design ==== ==== Software Design ====
 +
 +The system consists of two main software components: the embedded firmware running on the ESP32, developed using the Arduino IDE, and the Android application built with Kotlin. The ESP32 collects real-time health and location data from connected sensors, formats the information,​ and transmits it over Bluetooth. On the other side, the Android app receives this data, parses it, and presents it in a user-friendly interface for live monitoring.
 +
 === Arduino IDE Code === === Arduino IDE Code ===
 +
 +The firmware running on the ESP32 is responsible for collecting sensor data and sending it via Bluetooth. The setup initializes three main components: the MAX30100 pulse oximeter for measuring heart rate (BPM) and oxygen saturation (SpO₂), the Neo-6M GPS module for providing location data, and Bluetooth Serial for transmitting all collected values to the Android app.
 +
 +In the ''​loop()''​ function, the ESP32 continuously reads GPS data over serial in the NMEA 0183 format, focusing on ''​$GPRMC''​ sentences. These sentences contain comma-separated values representing time, fix status, latitude, longitude, and more. For example, from ''​$GPRMC,​200142.00,​A,​4426.63451,​N,​02603.24242,​E,​...'',​ the latitude value ''​4426.63451''​ and direction ''​N''​ are decoded into decimal degrees (''​44.443908''​) for easier mapping. At the same time, BPM and SpO₂ values are read once per second and all the data is sent over Bluetooth in a human-readable format.
 +
 +<​code>​
 +#include <​Wire.h>​
 +#include "​MAX30100_PulseOximeter.h"​
 +#include "​BluetoothSerial.h"​
 + 
 +// Define the RX and TX pins for Serial 2
 +#define RXD 16
 +#define TXD 17
 +#define SDA 21
 +#define SCL 22
 +#define GPS_BAUD 9600
 +
 +// Create an instance of the HardwareSerial class for Serial 2
 +HardwareSerial gpsSerial(2);​
 +
 +String gpsBuffer = "";​
 +
 +// Pulse sensor
 +PulseOximeter pox;
 +unsigned long lastPulseRead = 0;
 +const unsigned long pulseReadInterval = 1000; // 1 sec
 +
 +BluetoothSerial SerialBT;
 +
 +void setup() {
 +  // Serial Monitor
 +  Serial.begin(57600);​
 +
 +  SerialBT.begin("​RunTracker32"​);​
 +  Serial.println("​Bluetooth started..."​);​
 +  ​
 +  // Start Serial 2 with the defined RX and TX pins and a baud rate of 9600
 +  gpsSerial.begin(GPS_BAUD,​ SERIAL_8N1, RXD, TXD);
 +  Serial.println("​GPS Reader started..."​);​
 +  ​
 +  Wire.begin(SDA,​ SCL);
 +
 +  if (!pox.begin()) {
 +    Serial.println("​Failed to initialize MAX30100"​);​
 +    while (1);
 +  }
 +
 +  pox.setIRLedCurrent(MAX30100_LED_CURR_7_6MA);​
 +  Serial.println("​Pulse Oximeter started..."​);​
 +}
 +
 +void loop() {
 +  while (gpsSerial.available() > 0) {
 +    // get the byte data from the GPS
 +    char gpsData = gpsSerial.read();​
 +    ​
 +    if (gpsData == '​\n'​) {
 +      processGPSLine(gpsBuffer);​
 +      gpsBuffer = "";​
 +    } else if (gpsData != '​\r'​) {
 +      gpsBuffer += gpsData;
 +    }
 +  }
 +
 +  // --- Pulse sensor ---
 +  pox.update();​
 +
 +  if (millis() - lastPulseRead > pulseReadInterval) {
 +    lastPulseRead = millis();
 +    Serial.print("​BPM:​ ");
 +    Serial.print(pox.getHeartRate());​
 +    Serial.print(" ​ SpO2: ");
 +    Serial.println(pox.getSpO2());​
 +
 +    SerialBT.print("​BPM:​ ");
 +    SerialBT.print(pox.getHeartRate());​
 +    SerialBT.print(" ​ SpO2: ");
 +    SerialBT.println(pox.getSpO2());​
 +  }
 +}
 +
 +void processGPSLine(String line) {
 +  if (line.startsWith("​$GPRMC"​)) {
 +    Serial.println("​GPRMC line: " + line);
 +    String parts[12];
 +    int index = 0;
 +    int fromIndex = 0;
 +    int commaIndex;
 +
 +    while ((commaIndex = line.indexOf(',',​ fromIndex)) != -1 && index < 12) {
 +      parts[index++] = line.substring(fromIndex,​ commaIndex);​
 +      fromIndex = commaIndex + 1;
 +    }
 +
 +    // Extract latitude and longitude (if available)
 +    String latRaw = parts[3];
 +    String latDir = parts[4];
 +    String lonRaw = parts[5];
 +    String lonDir = parts[6];
 +
 +    if (latRaw != ""​ && lonRaw != ""​) {
 +      float lat = convertToDecimalDegrees(latRaw,​ latDir);
 +      float lon = convertToDecimalDegrees(lonRaw,​ lonDir);
 +      Serial.print("​Latitude:​ ");
 +      Serial.print(lat,​ 6);
 +      Serial.print(" ​ Longitude: ");
 +      Serial.println(lon,​ 6);
 +
 +      SerialBT.print("​Latitude:​ ");
 +      SerialBT.print(lat,​ 6);
 +      SerialBT.print(" ​ Longitude: ");
 +      SerialBT.println(lon,​ 6);
 +    } else {
 +      Serial.println("​Waiting for valid GPS fix..."​);​
 +    }
 +  }
 +}
 +
 +float convertToDecimalDegrees(String raw, String dir) {
 +  int degreeLength = (dir == "​N"​ || dir == "​S"​) ? 2 : 3;
 +  ​
 +  float deg = raw.substring(0,​ degreeLength).toFloat();​
 +  float min = raw.substring(degreeLength).toFloat();​
 +  float result = deg + (min / 60.0);
 +  ​
 +  if (dir == "​S"​ || dir == "​W"​)
 +    result = -result;
 +  return result;
 +}
 +
 +</​code>​
 +
 === Mobile App Code === === Mobile App Code ===
  
-==== Setup ====+The Android application is built using Kotlin and connects to the ESP32 via Bluetooth to display real-time fitness data. Upon launch, the app searches for a bonded device named RunTracker32,​ establishes a Bluetooth socket connection, and listens for incoming data in a background thread. 
 + 
 +The ''​listenForData()''​ function reads Bluetooth messages, and once a newline character is detected, the message is passed to ''​parseAndDisplay()''​. This function uses regular expressions to extract values for heart rate (BPM), oxygen saturation (SpO₂), and GPS coordinates. These values are then shown in the app's user interface. 
 + 
 +<​code>​ 
 +private fun listenForData() { 
 +  val input: InputStream? ​bluetoothSocket?​.inputStream 
 +  val buffer ​ByteArray(1024) 
 +  val messageBuilder ​StringBuilder() 
 + 
 +  while (true) { 
 +    val bytes input?​.read(buffer) ?: break 
 +    val readMessage ​String(buffer,​ 0, bytes) 
 + 
 +    messageBuilder.append(readMessage) 
 + 
 +    if (readMessage.contains("​\n"​)) { 
 +      val fullMessage ​messageBuilder.toString().trim() 
 +      messageBuilder.clear() 
 + 
 +      runOnUiThread { 
 +        parseAndDisplay(fullMessage) 
 +      } 
 +    } 
 +  } 
 +
 +</​code>​ 
 + 
 +<​code>​ 
 +private fun parseAndDisplay(message:​ String) { 
 +  val bpmRegex ​Regex("""​BPM:​\s*([\d.]+)"""​) 
 +  val spo2Regex ​Regex("""​SpO2:​\s*(\d+)"""​) 
 +  val latRegex = Regex("""​Latitude:​\s*([\d.]+)"""​) 
 +  val lonRegex = Regex("""​Longitude:​\s*([\d.]+)"""​) 
 + 
 +  val bpm = bpmRegex.find(message)?​.groupValues?​.get(1) ?: "?"​ 
 +  val spo2 = spo2Regex.find(message)?​.groupValues?​.get(1) ?: "?"​ 
 +  val latStr = latRegex.find(message)?​.groupValues?​.get(1) ?: ""​ 
 +  val lonStr = lonRegex.find(message)?​.groupValues?​.get(1) ?: ""​ 
 + 
 +  pulseText.text = getString(R.string.bpm_format,​ bpm) 
 +  spo2Text.text = getString(R.string.spo2_format,​ spo2) 
 + 
 +  if (latStr.isNotEmpty() && lonStr.isNotEmpty()) { 
 +    gpsText.text = getString(R.string.location_format,​ latStr, lonStr) 
 +    lastLat = latStr 
 +    lastLon = lonStr 
 + 
 +    val lat = latStr.toDoubleOrNull() 
 +    val lon = lonStr.toDoubleOrNull() 
 +    val now = System.currentTimeMillis() 
 + 
 +    if (lat != null && lon != null) { 
 +      if (lastLatValue != null && lastLonValue != null && lastTimestamp != null) { 
 +        val speed = calculateSpeedKmH( 
 +          lastLatValue!!,​ lastLonValue!!,​ lastTimestamp!!,​ 
 +          lat, lon, now 
 +        ) 
 +        speedText.text = getString(R.string.speed_format,​ "​%.2f"​.format(speed)) 
 +      } 
 +      lastLatValue = lat 
 +      lastLonValue = lon 
 +      lastTimestamp = now 
 +    } 
 +  } 
 +
 +</​code>​ 
 + 
 +To estimate speed, the app implements a Haversine-based function, calculateSpeedKmH(),​ which computes the distance between two sets of latitude/​longitude coordinates and divides it by the time difference. This allows the app to display an approximate speed in kilometers per hour (km/h), updated every time a new valid GPS coordinate is received. 
 + 
 +<​code>​ 
 +private fun calculateSpeedKmH( 
 +  lat1: Double, lon1: Double, time1: Long, 
 +  lat2: Double, lon2: Double, time2: Long 
 +): Double { 
 +  val deltaTimeSec = (time2 - time1) / 1000.0 
 +  if (deltaTimeSec == 0.0) return 0.0 
 + 
 +  val R = 6371.0 
 + 
 +  val dLat = Math.toRadians(lat2 - lat1) 
 +  val dLon = Math.toRadians(lon2 - lon1) 
 +  val a = sin(dLat / 2).pow(2.0) + 
 +      cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * 
 +      sin(dLon / 2).pow(2.0) 
 +  val c = 2 * atan2(sqrt(a),​ sqrt(1 - a)) 
 +  val distanceKm = R * c 
 + 
 +  return distanceKm / (deltaTimeSec / 3600.0) 
 +
 +</​code>​ 
 + 
 +==== Demonstration and Results ==== 
 + 
 +To illustrate the functionality of the system, the images and logs offer a glimpse into how the components interact in practice, with real-time data flowing from the hardware to the mobile app. 
 + 
 +The Android application displays real-time values such as heart rate, oxygen saturation, speed, and GPS location in a clean interface. 
 + 
 +{{:​iothings:​proiecte:​2025sric:​app.jpg?​300|}} 
 + 
 +Serial logs from the Arduino console show how data is continuously read from the GPS and pulse oximeter sensors. 
 + 
 +{{:​iothings:​proiecte:​2025sric:​monitor.png?​570|}}
  
-==== Results ====+the image shows the MAX30100 sensor in use for pulse measurement,​ alongside the GPS module actively providing location and speed data.
  
 +{{:​iothings:​proiecte:​2025sric:​testspeed.jpg?​570|}}
 ==== References ==== ==== References ====
  
 +  * [[https://​randomnerdtutorials.com/​esp32-neo-6m-gps-module-arduino/​|ESP32 with NEO-6M GPS Module (Arduino IDE)]]
 +  * [[https://​www.electronicwings.com/​esp32/​max30100-pulse-oximeter-interfacing-with-esp32|MAX30100 Pulse Oximeter Interfacing with ESP32]]
 +  * [[https://​www.ridgesolutions.ie/​index.php/​2013/​11/​14/​algorithm-to-calculate-speed-from-two-gps-latitude-and-longitude-points-and-time-difference/​|Algorithm to calculate speed from two GPS latitude and longitude points and time difference]]
iothings/proiecte/2025sric/runtrack32.1748426750.txt.gz · Last modified: 2025/05/28 13:05 by alexandra.cornea01
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0