/* 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 "freertos/queue.h"
#include "esp_timer.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 LED_DISPATCH = 26;
const int LED_BLOCKCLEAR = 27;
#define TRAIN_SENSOR_PIN 34
#define ESTOP_PIN 25
#define BRAKE_LED 4
#define HEARTBEAT_LED 2
bool dispatchLedState = false;
bool blockClearLedState = false;
SemaphoreHandle_t semEmergencyStop = nullptr;
SemaphoreHandle_t serialMutex = nullptr;
QueueHandle_t sensorQueue = nullptr;
const int OVERSPEED_THRESHOLD = 2500;
volatile bool rideEnabled = true;
volatile bool brakeEngaged = false;
volatile int lastTrainSensorValue = 0;
volatile int64_t isrEstopTimestampUs = 0;
portMUX_TYPE rideStateMux = portMUX_INITIALIZER_UNLOCKED;
void logLine(const String &msg) {
if (serialMutex && xSemaphoreTake(serialMutex, portMAX_DELAY) == pdTRUE) {
Serial.println(msg);
xSemaphoreGive(serialMutex);
} else {
Serial.println(msg);
}
}
void sendHtml() {
String response = R"(
<!DOCTYPE html><html>
<head>
<title>Orlando Coaster Safety Console</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html { font-family: sans-serif; text-align: center; }
body { margin-top: 20px; }
.btn { background-color: #5B5; border: none; color: #fff; padding: 0.5em 1em;
font-size: 2em; text-decoration: none; border-radius: 6px; }
.btn.OFF { background-color: #333; }
</style>
</head>
<body>
<h1>Orlando Coaster Safety Console</h1>
<h2>Ride Mode: MODE_TEXT</h2>
<a href="/toggleMode" class="btn">Toggle Mode (Enable/Maintenance)</a>
<h2>Train Load / Speed Sensor</h2>
<p style="font-size: 2em;">SENSOR_VALUE</p>
<h2>Brake State</h2>
<p style="font-size: 2em;">BRAKE_TEXT</p>
<h2>Panel LEDs</h2>
<div>
<h3>Dispatch</h3>
<a href="/toggle/1" class="btn LED1_TEXT">LED1_TEXT</a>
<h3>Block Clear</h3>
<a href="/toggle/2" class="btn LED2_TEXT">LED2_TEXT</a>
</div>
</body>
</html>
)";
response.replace("LED1_TEXT", dispatchLedState ? "ON" : "OFF");
response.replace("LED2_TEXT", blockClearLedState ? "ON" : "OFF");
response.replace("MODE_TEXT", rideEnabled ? "ENABLED" : "MAINTENANCE");
response.replace("SENSOR_VALUE", String(lastTrainSensorValue));
response.replace("BRAKE_TEXT", brakeEngaged ? "ENGAGED" : "RELEASED");
server.send(200, "text/html", response);
}
void toggleModeHandler() {
portENTER_CRITICAL(&rideStateMux);
rideEnabled = !rideEnabled;
bool snapshot = rideEnabled;
portEXIT_CRITICAL(&rideStateMux);
logLine(String("[RideCtrl] Ride mode changed via WEB to: ") +
(snapshot ? "ENABLED" : "MAINTENANCE"));
sendHtml();
}
void toggleLedHandler() {
String led = server.pathArg(0);
switch (led.toInt()) {
case 1:
dispatchLedState = !dispatchLedState;
digitalWrite(LED_DISPATCH, dispatchLedState);
break;
case 2:
blockClearLedState = !blockClearLedState;
digitalWrite(LED_BLOCKCLEAR, blockClearLedState);
break;
}
sendHtml();
}
void IRAM_ATTR emergencyStopISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
isrEstopTimestampUs = esp_timer_get_time();
brakeEngaged = true;
if (semEmergencyStop) {
xSemaphoreGiveFromISR(semEmergencyStop, &xHigherPriorityTaskWoken);
}
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
void taskTrainSensor(void* pv) {
pinMode(TRAIN_SENSOR_PIN, INPUT);
const TickType_t periodTicks = pdMS_TO_TICKS(20);
TickType_t lastWake = xTaskGetTickCount();
for (;;) {
vTaskDelayUntil(&lastWake, periodTicks);
int val = analogRead(TRAIN_SENSOR_PIN);
lastTrainSensorValue = val;
bool enabledSnapshot;
portENTER_CRITICAL(&rideStateMux);
enabledSnapshot = rideEnabled;
portEXIT_CRITICAL(&rideStateMux);
if (enabledSnapshot && !brakeEngaged && val > OVERSPEED_THRESHOLD) {
brakeEngaged = true;
digitalWrite(BRAKE_LED, HIGH);
if (sensorQueue) {
xQueueSend(sensorQueue, &val, 0);
}
logLine(String("[RideCtrl] OVERSPEED! sensor=") + val +
" (brakes from TrainSensor task)");
}
}
}
void taskEmergencyBrake(void* pv) {
for (;;) {
if (xSemaphoreTake(semEmergencyStop, portMAX_DELAY) == pdTRUE) {
int64_t nowUs = esp_timer_get_time();
int64_t delta = nowUs - isrEstopTimestampUs;
brakeEngaged = true;
digitalWrite(BRAKE_LED, HIGH);
logLine(String("[RideCtrl] E-STOP brake engaged. ISR->task latency(us)=") +
delta);
}
}
}
void taskHeartbeat(void* pv) {
pinMode(HEARTBEAT_LED, OUTPUT);
for (;;) {
digitalWrite(HEARTBEAT_LED, HIGH);
vTaskDelay(pdMS_TO_TICKS(1000));
digitalWrite(HEARTBEAT_LED, LOW);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void taskTelemetryLogger(void* pv) {
int sample = 0;
for (;;) {
if (sensorQueue &&
xQueueReceive(sensorQueue, &sample, pdMS_TO_TICKS(500)) == pdTRUE) {
unsigned long start = micros();
// Variable "work" based on sensor magnitude (up to ~512 iterations)
for (int i = 0; i < (sample & 0x1FF); ++i) {
__asm__ __volatile__("nop");
}
unsigned long duration = micros() - start;
logLine(String("[RideCtrl] Telemetry: sensor=") + sample +
" busy=" + duration + "us");
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_DISPATCH, OUTPUT);
pinMode(LED_BLOCKCLEAR, OUTPUT);
pinMode(BRAKE_LED, OUTPUT);
pinMode(HEARTBEAT_LED, OUTPUT);
pinMode(ESTOP_PIN, INPUT_PULLUP);
semEmergencyStop = xSemaphoreCreateBinary();
serialMutex = xSemaphoreCreateMutex();
sensorQueue = xQueueCreate(16, sizeof(int));
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/{}"), toggleLedHandler);
server.on("/toggleMode", toggleModeHandler);
server.begin();
Serial.println("HTTP server started");
attachInterrupt(digitalPinToInterrupt(ESTOP_PIN), emergencyStopISR, FALLING);
xTaskCreate(taskTrainSensor, "TrainSensor", 2048, NULL, 3, NULL); // HARD
xTaskCreate(taskEmergencyBrake, "EmergencyBrake", 2048, NULL, 4, NULL); // HARD, highest
xTaskCreate(taskHeartbeat, "Heartbeat", 2048, NULL, 1, NULL); // SOFT
xTaskCreate(taskTelemetryLogger, "Telemetry", 4096, NULL, 0, NULL); // SOFT + variable
}
void loop() {
server.handleClient();
delay(2);
}