/* 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 "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_continuous.h"
#include "esp_log.h"
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
// Defining the WiFi channel speeds up the connection:
#define WIFI_CHANNEL 6
WebServer server(80);
const int LED1 = 26;
const int LED2 = 27;
#define LED_GREEN GPIO_NUM_27
#define LED_BLUE GPIO_NUM_26
#define BUTTON_PIN GPIO_NUM_18
#define POT_ADC_CHANNEL ADC_CHANNEL_6 // GPIO34
#define MAX_COUNT_SEM 150
// Threshold for radiation sensor
#define RADIATION_THRESHOLD 1800
#define LOG_BUFFER_SIZE 50
// Shared buffer and index
static uint16_t sensor_log[LOG_BUFFER_SIZE];
static int log_index = 0;
// Handles for semaphores and mutex
SemaphoreHandle_t sem_button;
SemaphoreHandle_t sem_sensor;
SemaphoreHandle_t print_mutex;
SemaphoreHandle_t data_mutex;
SemaphoreHandle_t log_mutex;
adc_oneshot_unit_handle_t adc1_handle;
volatile int SEMCNT = 0;
volatile int currentRadiation = 0;
bool led1State = false;
bool led2State = false;
void heartbeat_task(void *pvParameters) {
bool led_status = false;
while (1) {
led_status = !led_status;
gpio_set_level(LED_GREEN, led_status);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void event_handler_task(void *pvParameters) {
while (1) {
if (xSemaphoreTake(sem_sensor, 0)) {
SEMCNT--; // DO NOT MODIFY THIS LINE
xSemaphoreTake(print_mutex, portMAX_DELAY);
printf("Radiation limit exceeded! Move away from source!\n");
xSemaphoreGive(print_mutex);
gpio_set_level(LED_BLUE, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(LED_BLUE, 0);
}
if (xSemaphoreTake(sem_button, 0)) {
xSemaphoreTake(print_mutex, portMAX_DELAY);
printf("MANUAL SYSTEM OVERRIDE. CONTINUE MEASUREMENT\n");
xSemaphoreGive(print_mutex);
gpio_set_level(LED_BLUE, 1);
vTaskDelay(pdMS_TO_TICKS(300));
gpio_set_level(LED_BLUE, 0);
}
vTaskDelay(pdMS_TO_TICKS(10)); // Idle delay to yield CPU
}
}
// button task
void override_task(void *pvParameters) {
// used AI here to brainstorm ways to debounce. Creating a state was what it suggested
int lastReading = 1;
int buttonState = 1;
TickType_t lastDebounceTime = 0;
const TickType_t debounceDelay = pdMS_TO_TICKS(50);
while (1) {
int reading = gpio_get_level(BUTTON_PIN);
// If reading changed, reset debounce timer
if (reading != lastReading) {
lastDebounceTime = xTaskGetTickCount();
}
// If stable long enough, treat as actual state
if ((xTaskGetTickCount() - lastDebounceTime) > debounceDelay) {
// Only trigger if the stable state changed
if (reading != buttonState) {
buttonState = reading;
// Detect press (HIGH → LOW)
if (buttonState == 0) {
xSemaphoreGive(sem_button);
if (xSemaphoreTake(print_mutex, portMAX_DELAY)) {
printf("Ground control alert:\n");
xSemaphoreGive(print_mutex);
}
}
}
}
lastReading = reading;
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// sensor task
void radiation_task(void *pvParameters) {
// Configure ADC channel
adc_oneshot_chan_cfg_t config = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
adc_oneshot_config_channel(adc1_handle, POT_ADC_CHANNEL, &config);
// Raw reading variable
int raw;
const TickType_t periodTicks = pdMS_TO_TICKS(300); // e.g. 500 ms period
TickType_t lastWakeTime = xTaskGetTickCount(); // initialize last wake time
bool wasAboveThreshold = false;
TickType_t now;
int jitter;
while (1) {
// Read current sensor value
adc_oneshot_read(adc1_handle, POT_ADC_CHANNEL, &raw);
now = xTaskGetTickCount();
jitter = now - lastWakeTime;
printf("Jitter: %d ticks\n", jitter);
lastWakeTime = now;
if (xSemaphoreTake(data_mutex, portMAX_DELAY)) {
currentRadiation = raw;
xSemaphoreGive(data_mutex);
}
xSemaphoreTake(print_mutex, portMAX_DELAY);
printf("Radiation Level: %d\n", raw);
xSemaphoreGive(print_mutex);
bool isAboveThreshold = (raw > RADIATION_THRESHOLD);
// Rising edge: was below, now above
if (isAboveThreshold && !wasAboveThreshold) {
if (SEMCNT < MAX_COUNT_SEM+1) SEMCNT++; // DO NOT REMOVE
xSemaphoreGive(sem_sensor);
}
// Update state
wasAboveThreshold = isAboveThreshold;
// Lock mutex to write value to buffer
if (xSemaphoreTake(log_mutex, pdMS_TO_TICKS(10))) {
sensor_log[log_index] = raw;
// this circular buffer will have issues at startup and when cleared; no clearing logic here
log_index = (log_index + 1) % LOG_BUFFER_SIZE;
xSemaphoreGive(log_mutex); //give up the semaphore!
}
vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(300));
}
}
void wifi_task(void *pvParameters) {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, WIFI_CHANNEL);
Serial.print("Connecting to WiFi ");
Serial.print(WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(100));
Serial.print(".");
}
Serial.println(" Connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// Setup routes
server.on("/", sendHtml);
server.on(UriBraces("/toggle/{}"), []() {
String led = server.pathArg(0);
switch (led.toInt()) {
case 1:
led1State = !led1State;
gpio_set_level(LED_BLUE, led1State);
break;
case 2:
led2State = !led2State;
gpio_set_level(LED_GREEN, led2State);
break;
}
sendHtml();
});
server.on("/radiation", []() {
int value;
if (xSemaphoreTake(data_mutex, portMAX_DELAY)) {
value = currentRadiation;
xSemaphoreGive(data_mutex);
}
server.send(200, "text/plain", String(value));
});
server.begin();
Serial.println("HTTP server started");
// Main loop of WiFi task
while (1) {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(2)); // yield CPU
}
}
void sendHtml() {
String response = R"(
<!DOCTYPE html>
<html>
<head>
<title>ESP32 Control + Radiation Monitor</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: 1em; }
.value { font-size: 2.5em; margin: 15px; }
div.controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
margin-top: 20px;
}
.btn {
background-color: #5B5;
border: none;
color: #fff;
padding: 0.5em 1em;
font-size: 1.5em;
text-decoration: none;
}
.btn.OFF { background-color: #333; }
</style>
</head>
<body>
<h1>ESP32 Monitor</h1>
<h2>Radiation Level</h2>
<div class="value" id="radiation">--</div>
<div class="controls">
<div>
<h2>LED 1</h2>
<a href="/toggle/1" class="btn LED1_TEXT">LED1_TEXT</a>
</div>
<div>
<h2>LED 2</h2>
<a href="/toggle/2" class="btn LED2_TEXT">LED2_TEXT</a>
</div>
</div>
<script>
function updateRadiation() {
fetch("/radiation")
.then(response => response.text())
.then(data => {
let val = parseInt(data);
let el = document.getElementById("radiation");
el.innerText = val;
if (val > 1800) {
el.style.color = "red";
} else {
el.style.color = "green";
}
});
}
setInterval(updateRadiation, 500);
updateRadiation();
</script>
</body>
</html>
)";
response.replace("LED1_TEXT", led1State ? "ON" : "OFF");
response.replace("LED2_TEXT", led2State ? "ON" : "OFF");
server.send(200, "text/html", response);
}
void setup(void) {
// Configure output LEDs
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_GREEN) | (1ULL << LED_BLUE),
.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
// Create ADC unit
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_config, &adc1_handle);
// Configure ADC channel
adc_oneshot_chan_cfg_t config = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
adc_oneshot_config_channel(adc1_handle, POT_ADC_CHANNEL, &config);
// Create sync primitives
sem_button = xSemaphoreCreateBinary();
sem_sensor = xSemaphoreCreateCounting(MAX_COUNT_SEM,0);
print_mutex = xSemaphoreCreateMutex();
data_mutex = xSemaphoreCreateMutex();
log_mutex = xSemaphoreCreateMutex();
assert(sem_button && sem_sensor && print_mutex && data_mutex && log_mutex);
// Create tasks
xTaskCreate(heartbeat_task, "heartbeat", 2048, NULL, 1, NULL);
xTaskCreate(radiation_task, "sensor", 2048, NULL, 2, NULL);
xTaskCreate(override_task, "button", 2048, NULL, 3, NULL);
xTaskCreate(event_handler_task, "event_handler", 2048, NULL, 2, NULL);
xTaskCreatePinnedToCore(wifi_task,"wifi_task", 4096, NULL, 1, NULL, 0);
}
void loop(void) {
vTaskDelay(portMAX_DELAY);
}