Astro/Starlight documentation covering hardware specs, pinout, schematics, GNSS protocol, LoRa frequencies, and getting started guides. Includes extracted datasheet images and Docker deployment.
295 lines
7.8 KiB
Markdown
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+
|