Community project

Med Matrix Sentinel Wearable

ESP32
Photo of Med Matrix Sentinel Wearable
Generated with AI

daksh pareek

Published September 1, 2026

The Med Matrix Sentinel Wearable is a health-monitoring smartwatch built around an ESP32 that tracks vital signs, environmental conditions, and detects emergencies. It combines optical pulse and oxygen sensing with air quality monitoring, motion detection, and temperature measurement to provide comprehensive health awareness on the wrist.

This guide covers the complete assembly of a multi-sensor wearable with dual communication paths: LoRa for long-range emergency relay and cellular/GPS for direct SOS alerts. You'll learn to wire the battery charging circuit, integrate I2C health sensors, configure the haptic feedback system, and program the firmware that evaluates sensor data to trigger alerts. The guide includes a wiring diagram, complete parts list, step-by-step assembly instructions, and tested firmware code.

Wiring diagram

Wiring diagram for Med Matrix Sentinel Wearable

Gather all the parts

QtyComponent
1

MAX86161 optical pulse and oxygen sensor module

MAX86161

A compact optical sensor module that reads pulse and blood-oxygen trends through the skin.

1

Haptic Driver

DRV2605L

DRV2605L haptic motor driver breakout for ERM/LRA vibration motors. Controlled over I2C at 0x5A, uses an internal waveform library, and drives the motor through OUT+/OUT- instead of direct MCU GPIO.

1

8 mm coin vibration motor

8 mm ERM coin motor

The thin round motor that can be felt through the wrist strap during warnings and SOS.

1

Push Button

momentary push button

Momentary push button switch

1

ADXL345 Accelerometer

ADXL345

Digital ADXL345 triple-axis accelerometer breakout with I2C/SPI interface, STEMMA QT/Qwiic connectors, on-board 3.3 V regulator, and logic-level shifting for 3 V or 5 V microcontrollers.

1

TMP117 Temperature Sensor

TMP117

Texas Instruments TMP117 high-accuracy, low-power digital temperature sensor with I2C/SMBus interface. Common breakouts default to address 0x48 and are used when a precise calibrated temperature reading is more important than a cheap DHT-style reading.

1

TP4056 Li-Ion/LiPo charger module with protection

USB-C, protected

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

3.3v Buck Boost

3.3 V, 1 A

TI TPS63030/TPS63031 high-efficiency single-inductor buck-boost converter family with 1A switches. Used to hold a regulated rail when battery voltage crosses above and below the target output.

1

BME280

BME280

Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed.

1

Bosch BMV080

PM1 / PM2.5 / PM10

Bosch Sensortec BMV080 ultra-mini fanless particulate matter (PM1 / PM2.5 / PM10) air quality sensor. Communicates over I2C (default) or SPI, selectable via on-board jumper on the SparkFun breakout. 3.3V supply. World's smallest PM sensor; firmware integration relies on the Bosch BMV080 SDK plus the SparkFun BMV080 Arduino Library wrapper.

1

RFM95W LoRa Transceiver Module (SX1276)

RFM95W SX1276, 865–867 MHz for India

HopeRF RFM95W long-range LoRa/LoRaWAN radio module based on Semtech SX1276. Typically used at 868 MHz (EU) or 915 MHz (US) ISM bands. SPI interface plus RESET and DIO interrupt lines. 3.3 V logic only. Requires an external antenna — never transmit without one.

1

3.7 V 1500 mAh high-discharge LiPo battery

3.7 V, 1500 mAh, protected, ≥3 A continuous discharge

A flat rechargeable battery that supplies the wearable and can safely provide short cellular transmit bursts.

1

SIM808 GSM/GPRS + GPS module

SIM808, GSM/GPRS + GPS, 3.4–4.4 V

A combined 2G cellular and GPS module that sends emergency SMS messages and obtains outdoor location.

1

4.0 V 3 A peak buck-boost regulator module

4.0 V regulated, ≥3 A peak

A dedicated high-current regulator that supplies the cellular module without pulling down the watch supply.

1

1000 µF low-ESR electrolytic capacitor

1000 µF, ≥6.3 V, low ESR

A local energy reservoir that supports the cellular module during short transmit current bursts.

1

4.7 kΩ resistor

4.7 kΩ, 1/10 W

The upper resistor in the serial voltage divider that reduces the XIAO transmit level for the SIM808 input.

1

27 kΩ resistor

27 kΩ, 1/10 W

The lower resistor in the serial voltage divider that reduces the XIAO transmit level for the SIM808 input.

Assemble it in 7 steps

1. Lay out the watch parts

Put the MAX86161 optical window through an opening in the watch back so it gently touches skin. Put TMP117 beside it, but keep it away from the battery. Place the BME280 and BMV080 behind separate mesh-covered side vents. Keep the SIM808, its cellular antenna, its GPS antenna, and the LoRa antenna along case edges, away from the optical sensor and battery foil.

  • A soft black foam ring around the MAX86161 opening blocks outside light and improves readings during movement.
  • GPS only works outdoors or near a clear view of the sky. LoRa is the fallback path in tunnels when GPS and mobile service cannot work.
  • Do not cover the BMV080 or BME280 vents with glue, a sleeve, or strap material; blocked vents give incorrect surrounding-air readings.
  • Do not make medical diagnoses from the optical readings; use this prototype for risk-pattern alerts.

2. Build the battery and charging path

Connect LiPo POSITIVE to TP4056 B+ (battery power) and LiPo NEGATIVE to TP4056 B− (battery ground). Connect TP4056 OUT+ to both regulator VIN pins (power), and TP4056 OUT− to the shared GND rail (ground). Connect the 3.3 V regulator VOUT to the 3V3 rail (power) for the XIAO, sensors, haptic driver, and LoRa module.

  • Before connecting electronics, measure the output rails with a multimeter: one must be 3.3 V and the cellular rail must be 4.0 V.
  • Use short, thick wires for the battery-to-cellular-regulator route.
  • Do not connect the LiPo directly to the XIAO charging pads; use only the TP4056 charging input.
  • Stop immediately if the battery becomes hot, swollen, or damaged; a damaged LiPo can catch fire.

3. Wire the shared health and air sensors

Connect MAX86161 VCC, DRV2605L VIN, ADXL345 VCC, TMP117 VIN, BME280 VCC, and BMV080 VDD to 3V3 (power). Connect all their GND pins to GND (ground). Connect every SDA pin to XIAO GPIO6 (data) and every SCL pin to XIAO GPIO7 (clock). Connect MAX86161 EN to 3V3 (keeps the sensor enabled).

  • Keep the GPIO6 and GPIO7 shared wires short and routed together.
  • Set the BME280 module to address 0x77 if it provides an SDO solder link.
  • Never connect these 3.3 V parts to the dedicated 4.0 V SIM808 rail; 4.0 V can damage them.

4. Wire the SOS button and vibration motor

Connect one SOS button leg to XIAO GPIO4 (signal) and its other leg to GND (ground). Connect the two coin-motor leads to DRV2605L OUT+ and OUT− (motor drive).

  • Holding the SOS button for about two seconds triggers an emergency sequence.
  • Either coin-motor wire can go to either motor output if vibration direction is not important.
  • Do not connect the coin motor directly to a XIAO pin; the DRV2605L is needed to drive it safely.

5. Wire the LoRa emergency relay radio

Connect RFM95W VCC to 3V3 (power) and GND to GND (ground). Connect MOSI to XIAO GPIO10 (data out), MISO to GPIO3 (data in), SCK to GPIO0 (clock), NSS to GPIO1 (radio select), RESET to GPIO2 (reset), and DIO0 to GPIO5 (packet signal). Attach a correctly matched 865–867 MHz antenna for India.

  • The radio sends short periodic heartbeats and emergency packets instead of listening continuously to conserve battery.
  • Use a fixed, independently powered LoRa relay chain in a tunnel; a wearable alone cannot transmit through rock or long bends.
  • GPIO0, GPIO1, and GPIO2 are startup-sensitive pins. Use an RFM95W module that does not pull these pins low at power-up, or the XIAO may fail to start.

6. Wire the SIM808 cellular and GPS section

Connect the 4.0 V regulator VOUT to SIM808 VCC (cellular power) and SIM808 GND to GND (ground). Place the 1000 µF capacitor directly beside SIM808: capacitor POSITIVE to SIM808 VCC (burst support), striped capacitor NEGATIVE to SIM808 GND (ground). Connect SIM808 TXD to XIAO GPIO20 (serial data). Connect GPIO21 through the 4.7 kΩ resistor to SIM808 RXD, then connect the 27 kΩ resistor from that same RXD junction to GND; this reduces the 3.3 V XIAO signal. Insert an active nano-SIM, attach the matched cellular antenna, and attach the matched GPS antenna.

  • Keep the capacitor leads and 4.0 V wiring as short as possible.
  • The watch samples GPS roughly every 20 minutes in normal use and requests an immediate fix after SOS; a location fix can take longer after the device has been indoors.
  • The SIM808 requires its own 4.0 V high-current rail. Do not power it from 3.3 V.
  • The capacitor stripe marks its negative lead. Reversing it can damage the capacitor.
  • Neither cellular nor GPS works reliably underground; LoRa relay nodes are required there.

7. Close the case and test the alert paths

Keep both air vents uncovered and route antennas along non-metal case edges. Charge through the TP4056 USB-C input. Outdoors, test a two-second SOS hold: the watch should vibrate, obtain a GPS fix when available, transmit a LoRa SOS packet, and text +91 8739951837 when cellular coverage is available. In a tunnel test, use installed LoRa relay nodes and verify that the packet reaches the entrance gateway.

  • Test cellular, GPS, and LoRa separately before relying on a combined emergency demonstration.
  • AQI and particle readings describe nearby air conditions; a high reading is a risk alert, not a complete toxic-gas analysis.
  • Do not wear or charge the open prototype in rain or near flammable vapors.
  • A successful project check does not prove local 2G coverage, GPS sky view, or tunnel relay coverage; test each at the demonstration location.

Review all connections

1. Connections between "lipo_battery" and "ESP32"

Functionlipo_batteryESP32
powerPOSITIVETP4056 Li-Ion/LiPo charger module with protection B+EXT
groundNEGATIVETP4056 Li-Ion/LiPo charger module with protection B-EXT

2. Connections between "lipo_charger" and "ESP32"

Functionlipo_chargerESP32
powerIN+USB-C 5 V charging sourceEXT
groundIN-GND
powerOUT+3.3v Buck Boost VINEXT
groundOUT-GND

3. Connections between "logic_regulator_3v3" and "ESP32"

Functionlogic_regulator_3v3ESP32
groundGNDGND
powerVOUT3V3

4. Connections between "gsm_regulator_4v" and "ESP32"

Functiongsm_regulator_4vESP32
powerVINTP4056 Li-Ion/LiPo charger module with protection OUT+EXT
groundGNDGND
powerVOUTSIM808 GSM/GPRS + GPS module VCCEXT

5. Connections between "gsm_bulk_capacitor" and "ESP32"

Functiongsm_bulk_capacitorESP32
powerPOSITIVESIM808 GSM/GPRS + GPS module VCCEXT
groundNEGATIVESIM808 GSM/GPRS + GPS module GNDEXT

6. Connections between "gsm_tx_series_resistor" and "ESP32"

Functiongsm_tx_series_resistorESP32
digitalAGPIO 21
digitalBSIM808 GSM/GPRS + GPS module RXDEXT

7. Connections between "gsm_tx_pull_down_resistor" and "ESP32"

Functiongsm_tx_pull_down_resistorESP32
digitalASIM808 GSM/GPRS + GPS module RXDEXT
groundBGND

8. Connections between "cellular_gnss_sim808" and "ESP32"

Functioncellular_gnss_sim808ESP32
groundGNDGND
uartTXDGPIO 20
dataSIMinstalled active nano-SIM cardEXT
dataGSM_ANTexternal matched 2G cellular antennaEXT
dataGPS_ANToutdoor GPS aerialEXT

9. Connections between "ppg_max30102" and "ESP32"

Functionppg_max30102ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7
powerEN3V3

10. Connections between "haptic_drv2605l" and "ESP32"

Functionhaptic_drv2605lESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7
dataOUT+8 mm coin vibration motor M+EXT
dataOUT-8 mm coin vibration motor M-EXT

11. Connections between "imu_adxl345" and "ESP32"

Functionimu_adxl345ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7

12. Connections between "temp_tmp117" and "ESP32"

Functiontemp_tmp117ESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7

13. Connections between "env_bme280" and "ESP32"

Functionenv_bme280ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7

14. Connections between "pm_bmv080" and "ESP32"

Functionpm_bmv080ESP32
powerVDD3V3
groundGNDGND
i2cSDAGPIO 6
i2cSCLGPIO 7

15. Connections between "sos_button" and "ESP32"

Functionsos_buttonESP32
groundGNDGND
digitalSIGNALGPIO 4

16. Connections between "lora_rfm95w" and "ESP32"

Functionlora_rfm95wESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 10
spiMISOGPIO 3
spiSCKGPIO 0
spiNSSGPIO 1
digitalRESETGPIO 2
digitalDIO0GPIO 5

Deploy the firmware

#define I2C_SCL 7
#define SOS_BUTTON_PIN 4
#define GSM_TX_PIN 21
#define LORA_MISO_PIN 3
#define LORA_SCK_PIN 0
#define LORA_RESET_PIN 2
#define LORA_DIO0_PIN 5
#define TINY_GSM_MODEM_SIM808
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <LoRa.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Adafruit_DRV2605.h>
#include <Adafruit_ADXL345_U.h>
#include <Adafruit_TMP117.h>
#include <Adafruit_BME280.h>
#include <TinyGsmClient.h>
#include <SparkFun_BMV080_Arduino_Library.h>
#include "maxm86161.h"


// Forward declarations
int pm25ToAqi(float p);
void buzz(uint8_t effect);
void publish();
void setRisk(String r);
void loraTransmit(const char *kind);
void updateGps(uint32_t timeoutMs);
void sendSms(const char *why);
void emergency(const char *why);
void sample();
void sampleAir();
void evaluate();
void setupBle();

constexpr int I2C_SDA = 6, I2C_SCL = 7, SOS_BUTTON_PIN = 4;
constexpr int GSM_RX_PIN = 20, GSM_TX_PIN = 21;
constexpr int LORA_MOSI_PIN = 10, LORA_MISO_PIN = 3, LORA_SCK_PIN = 0;
constexpr int LORA_NSS_PIN = 1, LORA_RESET_PIN = 2, LORA_DIO0_PIN = 5;
constexpr uint32_t SAMPLE_MS = 100, AIR_MS = 1000, SOS_HOLD_MS = 2000;
constexpr uint32_t GPS_PERIOD_MS = 20UL * 60UL * 1000UL;
constexpr uint32_t LORA_BEACON_MS = 15UL * 60UL * 1000UL;
constexpr float FALL_G = 2.35f, HIGH_SKIN_C = 38.0f, HIGH_AIR_C = 40.0f;
constexpr uint8_t BMV080_ADDR = 0x57;
constexpr long LORA_FREQUENCY_HZ = 866000000;
const char EMERGENCY_NUMBER[] = "+918739951837";
const char BLE_SERVICE_UUID[] = "6b5b1c4d-6b86-4d4b-8b19-5f1c3a020001";
const char BLE_STATUS_UUID[] = "6b5b1c4d-6b86-4d4b-8b19-5f1c3a020002";

Adafruit_DRV2605 haptic;
Adafruit_ADXL345_Unified accel(12345);
Adafruit_TMP117 tmp117;
Adafruit_BME280 bme;
SparkFunBMV080 bmv080;
HardwareSerial modemSerial(1);
TinyGsm modem(modemSerial);
BLECharacteristic *statusChar = nullptr;
bool hapticOK=false, accelOK=false, tmpOK=false, bmeOK=false, pmOK=false, modemOK=false, loraOK=false;
bool sos=false, smsSent=false, alertSent=false;
uint32_t buttonAt=0, lastSample=0, lastAir=0, lastGps=0, lastBeacon=0;
float hr=0, spo2=0, skinC=0, airC=0, humidity=0, pm25=0, motionG=1;
int aqi=-1;
String risk="STARTING", latitude="", longitude="";

int pm25ToAqi(float p) {
  if (p < 0) return -1;
  struct B{float cl,ch;int il,ih;}; const B b[]={{0,12,0,50},{12.1,35.4,51,100},{35.5,55.4,101,150},{55.5,150.4,151,200},{150.5,250.4,201,300},{250.5,350.4,301,400},{350.5,500.4,401,500}};
  p=floorf(p*10)/10; if(p>500.4) return 500; for(auto &x:b) if(p>=x.cl&&p<=x.ch) return round(((float)(x.ih-x.il)/(x.ch-x.cl))*(p-x.cl)+x.il); return -1;
}
void buzz(uint8_t effect){if(hapticOK){haptic.setWaveform(0,effect);haptic.setWaveform(1,0);haptic.go();}}
void publish(){if(!statusChar)return; String m="state="+risk+",hr="+String(hr,0)+",spo2="+String(spo2,0)+",skinC="+String(skinC,1)+",airC="+String(airC,1)+",humidity="+String(humidity,0)+",pm25="+String(pm25,1)+",aqi="+String(aqi)+",lat="+latitude+",lon="+longitude;statusChar->setValue(m.c_str());statusChar->notify();}
void setRisk(String r){if(r==risk)return;risk=r;if(r=="SOS")buzz(82);else if(r=="ALERT")buzz(47);publish();}
void loraTransmit(const char *kind){if(!loraOK)return; LoRa.beginPacket();LoRa.print("SENTINEL,");LoRa.print(kind);LoRa.print(",");LoRa.print(aqi);LoRa.print(",");LoRa.print(latitude);LoRa.print(",");LoRa.print(longitude);LoRa.endPacket();}
void updateGps(uint32_t timeoutMs){if(!modemOK)return; modem.sendAT("+CGNSPWR=1"); uint32_t until=millis()+timeoutMs; while(millis()<until){modem.sendAT("+CGNSINF");String line=modemSerial.readStringUntil('\n');if(line.indexOf("+CGNSINF:")>=0){int fields=0,start=line.indexOf(':')+1;String f[10];for(int i=start;i<(int)line.length()&&fields<10;i++){if(line[i]==','){fields++;continue;}f[fields]+=line[i];}if(f[1]=="1"&&f[3].length()>3&&f[4].length()>3){latitude=f[3];longitude=f[4];break;}}delay(1000);} modem.sendAT("+CGNSPWR=0"); publish();}
void sendSms(const char *why){if(smsSent||!modemOK)return;String t="Med Matrix Sentinel "+String(why)+". HR "+String(hr,0)+" bpm, SpO2 trend "+String(spo2,0)+"%, AQI "+String(aqi)+", GPS "+latitude+","+longitude+".";if(modem.sendSMS(EMERGENCY_NUMBER,t))smsSent=true;}
void emergency(const char *why){if(alertSent)return;alertSent=true;setRisk("SOS");updateGps(30000);loraTransmit("SOS");sendSms(why);}
void sample(){hrm_loop_process();int h=hrm_get_heart_rate(),o=hrm_get_spo2();if(h>=35&&h<=220)hr=h;if(o>=70&&o<=100)spo2=o;if(accelOK){sensors_event_t e;accel.getEvent(&e);motionG=sqrtf(e.acceleration.x*e.acceleration.x+e.acceleration.y*e.acceleration.y+e.acceleration.z*e.acceleration.z)/SENSORS_GRAVITY_STANDARD;if(motionG>=FALL_G)emergency("possible fall");}if(tmpOK){sensors_event_t e;tmp117.getEvent(&e);if(e.temperature>15&&e.temperature<45)skinC=e.temperature;}if(bmeOK){airC=bme.readTemperature();humidity=bme.readHumidity();}}
void sampleAir(){if(!pmOK||millis()-lastAir<AIR_MS)return;lastAir=millis();if(bmv080.readSensor()){pm25=bmv080.PM25();aqi=pm25ToAqi(pm25);publish();}}
void evaluate(){if(sos||alertSent)return;bool bad=hr>=130||(hr>=35&&hr<=40)||(spo2>=70&&spo2<=90)||skinC>=HIGH_SKIN_C||airC>=HIGH_AIR_C||aqi>=151;setRisk(bad?"ALERT":"OK");}
void setupBle(){BLEDevice::init("MedMatrix-Sentinel");BLEServer *s=BLEDevice::createServer();BLEService *v=s->createService(BLE_SERVICE_UUID);statusChar=v->createCharacteristic(BLE_STATUS_UUID,BLECharacteristic::PROPERTY_READ|BLECharacteristic::PROPERTY_NOTIFY);statusChar->addDescriptor(new BLE2902());v->start();BLEAdvertising *a=BLEDevice::getAdvertising();a->addServiceUUID(BLE_SERVICE_UUID);a->start();}
void setup(){Serial.begin(115200);pinMode(SOS_BUTTON_PIN,INPUT_PULLUP);Wire.begin(I2C_SDA,I2C_SCL);hrm_init();hapticOK=haptic.begin();if(hapticOK)haptic.selectLibrary(1);accelOK=accel.begin();if(accelOK)accel.setRange(ADXL345_RANGE_16_G);tmpOK=tmp117.begin();bmeOK=bme.begin(0x77);pmOK=bmv080.begin(BMV080_ADDR,Wire);if(pmOK){bmv080.init();pmOK=bmv080.setMode(SF_BMV080_MODE_CONTINUOUS);}modemSerial.begin(9600,SERIAL_8N1,GSM_RX_PIN,GSM_TX_PIN);delay(3000);modemOK=modem.testAT(10000);if(modemOK)modem.sendAT("+CMGF=1");SPI.begin(LORA_SCK_PIN,LORA_MISO_PIN,LORA_MOSI_PIN,LORA_NSS_PIN);LoRa.setPins(LORA_NSS_PIN,LORA_RESET_PIN,LORA_DIO0_PIN);loraOK=LoRa.begin(LORA_FREQUENCY_HZ);setupBle();setRisk("OK");buzz(1);}
void loop(){uint32_t n=millis();if(digitalRead(SOS_BUTTON_PIN)==LOW){if(!buttonAt)buttonAt=n;if(!sos&&n-buttonAt>=SOS_HOLD_MS){sos=true;emergency("SOS button held");}}else{buttonAt=0;sos=false;}if(n-lastSample>=SAMPLE_MS){lastSample=n;sample();sampleAir();evaluate();}if(n-lastGps>=GPS_PERIOD_MS){lastGps=n;updateGps(10000);}if(n-lastBeacon>=LORA_BEACON_MS){lastBeacon=n;loraTransmit("BEACON");}}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik