/* ESP32 HTTP IoT Server Example for Wokwi.com
https://wokwi.com/projects/320964045035274834
To test, you need the Wokwi IoT Gateway, as explained here:
https://docs.wokwi.com/guides/esp32-wifi#the-private-gateway
Then start the simulation, and open http://localhost:9080
in another browser tab.
Note that the IoT Gateway requires a Wokwi Club subscription.
To purchase a Wokwi Club subscription, go to https://wokwi.com/club
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <uri/UriBraces.h>
#include <stdio.h>
#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
// Defining the WiFi channel speeds up the connection:
#define WIFI_CHANNEL 6
#define GREEN_LED GPIO_NUM_5
#define RED_LED GPIO_NUM_4
#define BUTTON_PIN GPIO_NUM_18
#define POT_ADC_CHANNEL ADC1_CHANNEL_6 // GPIO34
#define MAX_COUNT_SEM 1
// found that 1 semaphore was enough to handle even the fastest of readings
#define HEARTBEAT_LOWER_THRESHOLD 60
#define HEARTBEAT_UPPER_THRESHOLD 100
// Handles for semaphores and mutex - you'll initialize these in the main program
SemaphoreHandle_t sem_button;
SemaphoreHandle_t sem_sensor;
SemaphoreHandle_t print_mutex;
WebServer server(80);
bool green_led_state = false;
bool red_led_state = false;
// prevents spamming, only prints on rising edge
static bool crossed_heartbeat_threshold = false;
static bool first_threshold_crossing = false;
static bool dead = false;
volatile int SEMCNT = 0; //You may not use this value in your logic -- but you can print it if you wish
void status_led_task(void *pvParameters) {
// indicates that the system is running normally
int led_state = 0;
while (1) {
led_state = !led_state;
gpio_set_level(GREEN_LED, led_state);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void heartbeat_sensor_task(void *pvParameters) {
while (1) {
// reading varies between 0 and 4095. A healthy heartbeat range is between 60 and 100 bpm,
// so I will scale this to have values between 0 and 195, which requires dividing by 21.
int val = adc1_get_raw(POT_ADC_CHANNEL);
int scaled_val = val / 21;
if(xSemaphoreTake(print_mutex, portMAX_DELAY)){
printf("heartbeat: %d bpm\n", scaled_val);
xSemaphoreGive(print_mutex);
}
// Rising edge detection with proper state tracking
// checks if heartbeat falls outside of healthy range and prints a warning and flashes the red LED
if (scaled_val > HEARTBEAT_UPPER_THRESHOLD || scaled_val < HEARTBEAT_LOWER_THRESHOLD) {
//check if the patient needs a defibulator
if(scaled_val == 0){
dead = true; // Hopefully someone gets there soon or RIP
}
else{
dead = false;
}
if(SEMCNT < MAX_COUNT_SEM+1) {
SEMCNT++;
xSemaphoreGive(sem_sensor);
if(!crossed_heartbeat_threshold){
first_threshold_crossing = true;
}
vTaskDelay(pdMS_TO_TICKS(200)); // so that the red LED blinks are even
}
crossed_heartbeat_threshold = true;
}
else {
crossed_heartbeat_threshold = false;
first_threshold_crossing = false; // Reset for next crossing
}
vTaskDelay(pdMS_TO_TICKS(100));
//for(int i = 0; i < 9999999; i++){} // simulates starvation of lower priority tasks due to unexpected processing time
}
}
void button_task(void *pvParameters) {
while (1) {
int state = gpio_get_level(BUTTON_PIN);
static TickType_t lastPressTime = 0; // (AI suggested using static to make sure it is not altered outside of the ISR)
TickType_t currentPressTime = xTaskGetTickCount(); //get the time of the most recent press for the debouncing
if((currentPressTime - lastPressTime) > pdMS_TO_TICKS(100)) { // 100ms debounce (AI)
// note: usually 50ms is a good enough time for debouncing but in the simulator it gives me multiple reads if i do less
if (state == 0){
xSemaphoreGive(sem_button);
if(xSemaphoreTake(print_mutex, portMAX_DELAY)){
printf("Button Pressed to alert doctor of unhealthy heart rate\n");
xSemaphoreGive(print_mutex);
}
}
lastPressTime = currentPressTime; // reset last press time for next debouncing comparison
}
vTaskDelay(pdMS_TO_TICKS(10)); // Do Not Modify This Delay!
}
}
void event_handler_task(void *pvParameters) {
while (1) {
if (xSemaphoreTake(sem_sensor, 0)) {
SEMCNT--; // DO NOT MODIFY THIS LINE
// Handle rising edge print
if(first_threshold_crossing) {
xSemaphoreTake(print_mutex, portMAX_DELAY);
printf("Threshold crossed! (First detection)\n");
xSemaphoreGive(print_mutex);
first_threshold_crossing = false; // Clear flag
}
if(dead){
printf("PATIENT'S HEART HAS STOPPED, BRING A DEFIBULATOR!\n");
}
gpio_set_level(RED_LED, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(RED_LED, 0);
}
if (xSemaphoreTake(sem_button, 0)) {
xSemaphoreTake(print_mutex, portMAX_DELAY);
printf("Calling a doctor to the patient!\n");
xSemaphoreGive(print_mutex);
gpio_set_level(RED_LED, 1);
vTaskDelay(pdMS_TO_TICKS(300));
gpio_set_level(RED_LED, 0);
}
vTaskDelay(pdMS_TO_TICKS(10)); // Idle delay to yield CPU
}
}
void sendHtml() {
String response = R"(
<!DOCTYPE html><html>
<head>
<title>ESP32 Web Server Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html { font-family: sans-serif; text-align: center; }
body { display: inline-flex; flex-direction: column; }
h1 { margin-bottom: 1.2em; }
h2 { margin: 0; }
div { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; grid-auto-flow: column; grid-gap: 1em; }
.btn { background-color: #5B5; border: none; color: #fff; padding: 0.5em 1em;
font-size: 2em; text-decoration: none }
.btn.OFF { background-color: #333; }
</style>
</head>
<body>
<h1>ESP32 Web Server</h1>
<div>
<h2>LED 1</h2>
<a href="/toggle/1" class="btn LED1_TEXT">LED1_TEXT</a>
<h2>LED 2</h2>
<a href="/toggle/2" class="btn LED2_TEXT">LED2_TEXT</a>
</div>
</body>
</html>
)";
response.replace("LED1_TEXT", green_led_state ? "ON" : "OFF");
response.replace("LED2_TEXT", red_led_state ? "ON" : "OFF");
server.send(200, "text/html", response);
}
void setup(void) {
// app 4 setup
// Create synchronization primitives
sem_button = xSemaphoreCreateBinary();
sem_sensor = xSemaphoreCreateCounting(MAX_COUNT_SEM, 0);
print_mutex = xSemaphoreCreateMutex();
assert(sem_button && sem_sensor && print_mutex);
// Configure output LEDs
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << GREEN_LED) | (1ULL << RED_LED),
.mode = GPIO_MODE_OUTPUT,
};
gpio_config(&io_conf);
// Configure input button
gpio_config_t btn_conf = {
.pin_bit_mask = (1ULL << BUTTON_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE
};
gpio_config(&btn_conf);
// Configure ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(POT_ADC_CHANNEL, ADC_ATTEN_DB_11);
//new setup
Serial.begin(115200);
while(!Serial){
Serial.println("not working");
}
Serial.println("Test");
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, WIFI_CHANNEL);
Serial.print("Connecting to WiFi ");
Serial.print(WIFI_SSID);
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.println(" Connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
server.on("/", sendHtml);
server.on(UriBraces("/toggle/{}"), []() {
String led = server.pathArg(0);
Serial.print("Toggle LED #");
Serial.println(led);
switch (led.toInt()) {
case 1:
green_led_state = !green_led_state;
digitalWrite(GREEN_LED, green_led_state);
break;
case 2:
red_led_state = !red_led_state;
digitalWrite(RED_LED, red_led_state);
break;
}
sendHtml();
});
server.begin();
Serial.println("HTTP server started");
// Create tasks
xTaskCreate(status_led_task, "status_led", 2048, NULL, 1, NULL);
xTaskCreate(heartbeat_sensor_task, "sensor", 2048, NULL, 2, NULL);
xTaskCreate(button_task, "button", 2048, NULL, 3, NULL);
xTaskCreate(event_handler_task, "event_handler", 2048, NULL, 4, NULL);
}
void loop(void) {
// main loop
server.handleClient();
delay(2);
}