Final Project Documentation (Video + Stills + Code)

Video:

 

Stills:

Code:

#include “esp_wifi.h”
#include “esp_wifi_types.h”
#include “esp_system.h”
#include “esp_event.h”
#include “esp_event_loop.h”
#include “nvs_flash.h”
#include <stdio.h>
// screen
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#define i2c_Address 0x3c
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // QT-PY / XIAO
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Define a fixed-size array to store BPM history
#define HISTORY_SIZE 24
int bpmHistory[HISTORY_SIZE] = { 0 };
// Adafruit Neopixel Settings
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif
#define LED_PIN 5
#define LED_COUNT 1
Adafruit_NeoPixel leds(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
uint32_t col;
#define SNAP_LEN 2324 // Maximum length of each received packet
#define VIBRATION_PIN 17
/* ===== run-time variables ===== */
uint32_t tmpPacketCounter; // Temporary packet counter
int currentHeartbeatInterval = 300; // Starting interval
int dynamicShortDelay = 170;
/* Callback function for Wi-Fi promiscuous mode */
void wifi_promiscuous(void* buf, wifi_promiscuous_pkt_type_t type) {
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
wifi_pkt_rx_ctrl_t ctrl = (wifi_pkt_rx_ctrl_t)pkt->rx_ctrl;
if (type == WIFI_PKT_MISC) return; // Ignore miscellaneous packets
if (ctrl.sig_len > SNAP_LEN) return; // Ignore packets too long
tmpPacketCounter++; // Increment packet counter
}
/* plotting functions */
void updateBpmHistory(int newBpm) {
// Shift the old values
for (int i = 0; i < HISTORY_SIZE – 1; i++) {
bpmHistory[i] = bpmHistory[i + 1];
}
// Add the new value to the end
bpmHistory[HISTORY_SIZE – 1] = newBpm;
}
void drawChart() {
// Starting position for the chart
int startX = 0;
int startY = 20; // Below the text
int chartHeight = 40; // Height of the chart area
// Find the max and min BPM in the history for dynamic scaling
int maxBpm = bpmHistory[0];
int minBpm = bpmHistory[0];
for (int i = 1; i < HISTORY_SIZE; i++) {
if (bpmHistory[i] > maxBpm) {
maxBpm = bpmHistory[i];
}
if (bpmHistory[i] < minBpm) {
minBpm = bpmHistory[i];
}
}
// Prevent division by zero and ensure there’s a range
if (maxBpm == minBpm) {
maxBpm = minBpm + 1; // Ensure there’s at least a range of 1
}
for (int i = 0; i < HISTORY_SIZE – 1; i++) {
// Map the BPM values to the chart area
int yCurrent = map(bpmHistory[i], minBpm, maxBpm, startY + chartHeight, startY);
int yNext = map(bpmHistory[i + 1], minBpm, maxBpm, startY + chartHeight, startY);
// Draw a line from each BPM value to the next
display.drawLine(startX + i * 5, yCurrent, startX + (i + 1) * 5, yNext, SH110X_WHITE);
}
}
/* ===== main program ===== */
void setup() {
// Initialize Serial
Serial.begin(115200);
display.begin(i2c_Address, true); // Address 0x3C default
display.display();
delay(1000);
display.clearDisplay();
//Settings for the LED pixels
leds.begin(); // INITIALIZE NeoPixel leds object (REQUIRED)
leds.show(); // Turn OFF all pixels ASAP
leds.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)
col = leds.Color(119, 0, 200); //Set a purple color for the LEDs
pinMode(VIBRATION_PIN, OUTPUT); //vibration motor pin
// Initialize NVS, TCP/IP adapter, and Wi-Fi
nvs_flash_init();
tcpip_adapter_init();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_event_loop_init(NULL, NULL));
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL));
ESP_ERROR_CHECK(esp_wifi_start());
esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&wifi_promiscuous);
}
void loop() {
delay(200); // Reduced wait time
Serial.print(“PPS:”);
Serial.print(int(tmpPacketCounter) * 5);
Serial.print(” “);
Serial.print(int(tmpPacketCounter));
Serial.print(“\t”);
// Map packet count to desired delay between heartbeats
int desiredHeartbeatInterval = map(constrain(tmpPacketCounter, 0, 150), 0, 150, 1000, 10);
// Smooth the transition using a weighted average
currentHeartbeatInterval = (0.9 * currentHeartbeatInterval) + (0.1 * desiredHeartbeatInterval);
// Calculate the dynamic short delay between double taps/blinks
int desiredShortDelay = map(constrain(tmpPacketCounter, 0, 120), 0, 120, 200, 50);
dynamicShortDelay = (0.8 * dynamicShortDelay) + (0.2 * desiredShortDelay);
// Calculate BPM
int bpm = 60000 / (currentHeartbeatInterval + 200 + dynamicShortDelay + 120); // Calculate Beats Per Minute
updateBpmHistory(bpm);
Serial.print(“BPM:”);
Serial.println(bpm);
// Start a display buffer
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SH110X_WHITE); // Draw white text
display.setCursor(0, 0); // Start at top-left corner
display.print(“BPM: “);
display.print(bpm);
display.print(” “);
display.print(“PPS: “);
display.println(int(round(tmpPacketCounter * 3.3)));
// Draw the BPM chart
drawChart();
display.display();
vibrateMotor(dynamicShortDelay);
LEDblink(dynamicShortDelay);
delay(currentHeartbeatInterval); // Smoothed delay between heartbeats
tmpPacketCounter = 0; // Reset packet counter
}
void vibrateMotor(int shortDelay) {
// Double tap vibration with dynamic short delay
for (int i = 0; i < 2; i++) {
digitalWrite(VIBRATION_PIN, HIGH);
delay(60);
digitalWrite(VIBRATION_PIN, LOW);
if (i == 0) {
delay(shortDelay); // Dynamic short delay between double taps
}
}
}
void LEDblink(int shortDelay) {
// Double tap LED blink with dynamic short delay
for (int i = 0; i < 2; i++) {
leds.setPixelColor(0, col);
leds.show();
delay(60);
leds.clear();
leds.show();
if (i == 0) {
delay(shortDelay); // Dynamic short delay between double blinks
}
}
}