Differences

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

Link to this comparison view

iothings:proiecte:2025sric:runtrack32 [2025/05/28 12:53]
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 
 +  * Email: ​    ​alexandra.cornea01@stud.acs.upb.ro 
 +  * Master: ​   SRIC1 
 + 
 +==== 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 ==== 
 + 
 +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 ==== 
 + 
 +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 === 
 + 
 +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..."​);​
   ​   ​
-  ​* **Author:**    Alexandra Cornea +  ​// Start Serial 2 with the defined RX and TX pins and a baud rate of 9600 
-  * **Email:**     ​alexandra.cornea01@stud.acs.upb.ro +  gpsSerial.begin(GPS_BAUD,​ SERIAL_8N1, RXD, TXD); 
-  * **Master:**    SRIC1+  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 === 
 + 
 +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 
 + 
 +  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|}} 
 + 
 +the image shows the MAX30100 sensor in use for pulse measurement,​ alongside the GPS module actively providing location and speed data.
  
-===== 1. Introduction =====+{{:​iothings:​proiecte:​2025sric:​testspeed.jpg?​570|}} 
 +==== 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.1748425983.txt.gz · Last modified: 2025/05/28 12:53 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