Heart Rate Monitor via LoRaWAN
This project enables real-time heart rate monitoring using the Wireless Tracker connected to a MAX30102 pulse oximetry sensor. Data transmits via LoRaWAN to the SnapEmu platform for visualization.
Hardware Requirements
Section titled “Hardware Requirements”| Component | Purpose |
|---|---|
| Heltec Wireless Tracker | Main controller + LoRa TX |
| HT-M7603 Indoor LoRa Gateway | LoRaWAN network gateway |
| MAX30102 Heart Rate Sensor | Pulse oximetry sensor |
| DuPont Wires | Connections |
Wiring Diagram
Section titled “Wiring Diagram”The MAX30102 connects via I2C:
| MAX30102 Pin | Tracker Pin | Description |
|---|---|---|
| VCC | 3V3 | Power (3.3V) |
| GND | GND | Ground |
| SDA | GPIO45 | I2C Data |
| SCL | GPIO46 | I2C Clock |
Software Setup
Section titled “Software Setup”-
Configure LoRa Gateway
Set up your HT-M7603 gateway with SnapEmu platform following the official Heltec documentation.
-
Register Device on SnapEmu
Register your Wireless Tracker node following the SnapEmu device connection guide.
-
Install Libraries
Required Arduino libraries:
Wire.h(built-in)MAX30102_PulseOximeter.hLoRaWan_APP.h(Heltec library)
-
Test Sensor Standalone
Upload the test code to verify sensor operation before adding LoRaWAN.
-
Deploy Full Application
Upload the complete LoRaWAN code with your OTAA credentials.
Code Examples
Section titled “Code Examples”#include <Wire.h>#include "MAX30102_PulseOximeter.h"
#define REPORTING_PERIOD_MS 1000PulseOximeter pox;uint32_t tsLastReport = 0;
void onBeatDetected() { Serial.println("Beat detected!");}
void setup() { Serial.begin(115200); Wire.begin(45, 46); // SDA=GPIO45, SCL=GPIO46 Serial.print("Initializing MAX30102.."); delay(3000);
if (!pox.begin()) { Serial.println("FAILED"); for(;;); } else { Serial.println("SUCCESS"); }
pox.setIRLedCurrent(MAX30102_LED_CURR_7_6MA); pox.setOnBeatDetectedCallback(onBeatDetected);}
void loop() { pox.update();
if (millis() - tsLastReport > REPORTING_PERIOD_MS) { float heartRate = pox.getHeartRate(); float spo2 = pox.getSpO2();
Serial.print("HR: "); Serial.print(heartRate > 0 ? String(heartRate) : "N/A"); Serial.print(" bpm / SpO2: "); Serial.print(spo2 > 0 ? String(spo2) : "N/A"); Serial.println("%");
tsLastReport = millis(); }}#include <Wire.h>#include "MAX30102_PulseOximeter.h"#include "LoRaWan_APP.h"
#define REPORTING_PERIOD_MS 1000
// OTAA Parameters - Replace with your SnapEmu valuesuint8_t devEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA8 };uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };uint8_t appKey[] = { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88 };
// LoRaWAN Configurationuint16_t userChannelsMask[6] = { 0x00FF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 };LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;DeviceClass_t loraWanClass = CLASS_A;uint32_t appTxDutyCycle = 15000; // 15 second transmit intervalbool overTheAirActivation = true;bool loraWanAdr = true;bool isTxConfirmed = true;uint8_t appPort = 2;uint8_t confirmedNbTrials = 4;
PulseOximeter pox;uint32_t tsLastReport = 0;
static void prepareTxFrame(uint8_t port) { pox.update(); float heartRate = pox.getHeartRate(); float spo2 = pox.getSpO2(); unsigned char *puc;
appDataSize = 0;
// Packet header appData[appDataSize++] = 0x04; appData[appDataSize++] = 0x00; appData[appDataSize++] = 0x0A; appData[appDataSize++] = 0x02;
// Heart rate (4 bytes float) puc = (unsigned char *)(&heartRate); appData[appDataSize++] = puc[0]; appData[appDataSize++] = puc[1]; appData[appDataSize++] = puc[2]; appData[appDataSize++] = puc[3];
// Separator appData[appDataSize++] = 0x12;
// SpO2 (4 bytes float) puc = (unsigned char *)(&spo2); appData[appDataSize++] = puc[0]; appData[appDataSize++] = puc[1]; appData[appDataSize++] = puc[2]; appData[appDataSize++] = puc[3];
Serial.printf("TX: HR=%.1f SpO2=%.1f\n", heartRate, spo2);}
void onBeatDetected() { Serial.println("Beat!");}
void setup() { Serial.begin(115200); Wire.begin(45, 46); Mcu.begin(HELTEC_BOARD, SLOW_CLK_TPYE);
Serial.print("Initializing MAX30102.."); delay(3000);
if (!pox.begin()) { Serial.println("FAILED"); for(;;); } Serial.println("SUCCESS");
pox.setIRLedCurrent(MAX30102_LED_CURR_27_1MA); pox.setOnBeatDetectedCallback(onBeatDetected);
LoRaWAN.init(loraWanClass, loraWanRegion); LoRaWAN.setDefaultDR(3);}
void loop() { pox.update();
// Local display if (millis() - tsLastReport > REPORTING_PERIOD_MS) { float heartRate = pox.getHeartRate(); float spo2 = pox.getSpO2();
Serial.printf("HR: %s bpm | SpO2: %s%%\n", heartRate > 0 ? String(heartRate).c_str() : "N/A", spo2 > 0 ? String(spo2).c_str() : "N/A");
tsLastReport = millis(); }
// LoRaWAN state machine switch (deviceState) { case DEVICE_STATE_INIT: case DEVICE_STATE_JOIN: LoRaWAN.join(); break; case DEVICE_STATE_SEND: prepareTxFrame(appPort); LoRaWAN.send(); deviceState = DEVICE_STATE_CYCLE; break; case DEVICE_STATE_CYCLE: txDutyCycleTime = appTxDutyCycle + randr(-APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND); LoRaWAN.cycle(txDutyCycleTime); deviceState = DEVICE_STATE_SLEEP; break; case DEVICE_STATE_SLEEP: LoRaWAN.sleep(loraWanClass); break; default: deviceState = DEVICE_STATE_INIT; break; }}// SnapEmu payload decoderfunction Decoder(bytes, port) { var decoded = {};
if (bytes.length >= 13) { // Extract heart rate (bytes 4-7) var hrBytes = new Uint8Array([bytes[4], bytes[5], bytes[6], bytes[7]]); var hrView = new DataView(hrBytes.buffer); decoded.heartRate = hrView.getFloat32(0, true);
// Extract SpO2 (bytes 9-12) var spo2Bytes = new Uint8Array([bytes[9], bytes[10], bytes[11], bytes[12]]); var spo2View = new DataView(spo2Bytes.buffer); decoded.spo2 = spo2View.getFloat32(0, true); }
return decoded;}Platform Features
Section titled “Platform Features”SnapEmu provides:
- Real-time heart rate graph
- SpO2 level monitoring
- Historical data (up to 1 month)
- Mobile app + web interface
Troubleshooting
Section titled “Troubleshooting”| Issue | Solution |
|---|---|
| No heartbeat detected | Ensure finger is properly placed, adjust LED current |
| Erratic readings | Clean sensor, reduce movement, check power supply |
| LoRa join failed | Verify gateway connection, check OTAA credentials |
| Data not on platform | Check decoder function, verify port number matches |
References
Section titled “References”Based on the project by ashley15 on Hackster.io.