This shows you the differences between two versions of the page.
iothings:proiecte:2025sric:runtrack32 [2025/05/29 00:42] alexandra.cornea01 [Software Design] |
iothings:proiecte:2025sric:runtrack32 [2025/05/29 02:31] (current) alexandra.cornea01 [Demonstration and Results] |
||
---|---|---|---|
Line 17: | Line 17: | ||
The sensors are powered via the breadboard's power rails, and communication is established using serial (for GPS) and I2C (for MAX30100). | 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?500|}} | + | {{:iothings:proiecte:2025sric:runtracker.jpg?570|}} |
**GPS (Neo-6M) Connections** | **GPS (Neo-6M) Connections** | ||
Line 35: | Line 35: | ||
=== 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]] |