Ryan Malloy e3642cbc4e Initial commit: Heltec Wireless Tracker documentation site
Astro/Starlight documentation covering hardware specs, pinout,
schematics, GNSS protocol, LoRa frequencies, and getting started
guides. Includes extracted datasheet images and Docker deployment.
2026-02-21 18:39:46 -07:00

295 lines
7.8 KiB
Markdown

# 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 <Wire.h>
#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 <Wire.h>
#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+