# Monitor Heart Rate on SnapEmu with Wireless Tracker ## Overview This project enables real-time heart rate monitoring using a wireless tracker connected to a heart rate sensor. Data transmits via LoRaWAN protocol to the SnapEmu platform for visualization and analysis. ## Hardware Components Required | 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 | ## Hardware Configuration ### Pin Connections The MAX30102 sensor connects to the Heltec Wireless Tracker using I2C: | MAX30102 Pin | Wireless Tracker Pin | Description | |--------------|---------------------|-------------| | VCC | 3V3 | Power (3.3V) | | GND | GND | Ground | | SDA | GPIO45 | I2C Data | | SCL | GPIO46 | I2C Clock | > **Note:** GPIO45 and GPIO46 are recommended as they don't have conflicting built-in functions. ## Software Setup ### Step 1: Configure HT-M7603 Gateway Configure your LoRa gateway connection to SnapEmu platform: - Follow [official Heltec documentation](https://docs.heltec.cn/en/gateway/ht-m7603/connect_to_snapemu.html) ### Step 2: Register Tracker Node Register your wireless tracker device on SnapEmu: - Follow [node device connection guidelines](https://snapemudoc.readthedocs.io/en/latest/Node%20Devices%20Connection/register_on_snapemu.html) ### Step 3: Install Libraries Required Arduino libraries: - `Wire.h` (built-in) - `MAX30102_PulseOximeter.h` - `LoRaWan_APP.h` (Heltec library) ### Step 4: Sensor Test Code First, test the sensor standalone: ```cpp #include #include "MAX30102_PulseOximeter.h" #define REPORTING_PERIOD_MS 1000 PulseOximeter 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(); if (heartRate > 0) { Serial.print("Heart rate: "); Serial.print(heartRate); Serial.print(" bpm / "); } else { Serial.print("Heart rate: N/A / "); } if (spo2 > 0) { Serial.print("SpO2: "); Serial.print(spo2); Serial.println(" %"); } else { Serial.println("SpO2: N/A"); } tsLastReport = millis(); } } ``` ### Step 5: Full LoRaWAN Code Complete code with LoRaWAN transmission: ```cpp #include #include "MAX30102_PulseOximeter.h" #include "LoRaWan_APP.h" #define REPORTING_PERIOD_MS 1000 // OTAA Parameters - Replace with your values from SnapEmu uint8_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 Configuration uint16_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 interval bool 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.print("TX: HR="); Serial.print(heartRate); Serial.print(" SpO2="); Serial.println(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.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(); } // 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; } } ``` ## Data Decoding on SnapEmu Custom decoder function for the payload format: ```javascript function 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 Visualization SnapEmu displays: - Real-time heart rate graph - SpO2 level monitoring - Historical data (up to 1 month) - Mobile app + web interface ## 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 credentials | | Data not showing on platform | Check decoder function, verify port number | ## References - [SnapEmu Documentation](https://snapemudoc.readthedocs.io/) - [Heltec Sensor Decoding](https://docs.heltec.cn/general/define_sensor_decoding_function_on_snapemu.html) - Original project by ashley15 on Hackster.io **License:** GPL3+