/*
* Application 5 — Dual-core IPC pipeline
*
* Scaffold level: ~65% (the pipeline logic is yours; the infrastructure is provided).
*
* Scaffold Code - AI useage:
* Addition of the USE_WEBSERVER compile-time switch and a working serial
* monitor task on Core 0, plus per-task heartbeat counters
* Logic to allow for switching between a serial monitor and the (student-built)
* web monitor, so the pipeline runs in Wokwi with no Wi-Fi by default
* Commenting of code including human readable summaries
*
* The three IPC primitives are CREATED and wired; the pipeline LOGIC is yours:
* Queue — fixed-size FIFO between producer & consumer
* Task notification — fast 1-to-1 signal (ISR or task → specific task)
* Event group — N-way rendezvous (wait for a set of bits)
* Required by the assignment: at least one use of each, EACH defended.
*
* What this scaffold gives you:
* - Producer / consumer / coordinator / responder task skeletons on Core 1.
* - The event-group rendezvous + coordinator→responder notification, wired.
* - A button ISR → responder direct-notification path.
* - Per-task heartbeat counters and a Core-0 monitor that prints queue depth,
* event-group bits, and heartbeats once a second (USE_WEBSERVER=0).
*
* What you do:
* 1. Producer body — themed data items, queue send with a back-pressure policy.
* 2. Consumer body — receive with timeout, themed processing.
* 3. Web monitor — port App 1's HTTP code into webmonitor_task (USE_WEBSERVER=1).
* 4. Size the queue (depth + item size) and defend it.
* 5. Theme-rename (YOURTHEME): task names, log strings, the meaning of a data item.
*
* What you DON'T need to change:
* - The event-group / notification plumbing, the button ISR, or the monitor.
* - The heartbeat counters (already incremented at the end of each task loop).
*
* ============================================================
* RUN MODE (serial monitor vs. web monitor)
* ============================================================
*
* USE_WEBSERVER selects the Core-0 observability plane. The Core-1 pipeline is
* identical in both modes.
*
* USE_WEBSERVER = 0 -> Serial monitor (provided, working). Prints queue depth,
* event bits, and heartbeats once a second. No Wi-Fi, so
* the pipeline runs in Wokwi out of the box.
* USE_WEBSERVER = 1 -> Web monitor. Runs webmonitor_task instead. Needs the
* Wi-Fi REQUIRES already in this folder's CMakeLists.
*
* Start on USE_WEBSERVER=0 to get the pipeline moving in the simulator, then flip
* to 1 once you have implemented the web monitor.
*
* ============================================================
* Theme: Perimeter Defense
* Producer = perimeter sensor
* Queue item = sensor contact report
* Consumer = threat-analysis unit
* Coordinator = confirms detection and analysis completed
* Responder = dispatches the response team
* ============================================================
*/
#ifndef USE_WEBSERVER
#define USE_WEBSERVER 0
#endif
#include <stdio.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_attr.h"
#if USE_WEBSERVER
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_http_server.h"
#endif
#define TEST_NOTIFICATION 0
#define TEST_SEMAPHORE 1
#define BUTTON_GPIO GPIO_NUM_18
#define ISR_PULSE_GPIO GPIO_NUM_19
#define DEBOUNCE_US 200000
#if USE_WEBSERVER
#define HTTP_PORT 80
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASS ""
#endif
#define CONFIG_LOG_DEFAULT_LEVEL_INFO 1
#define CONFIG_LOG_MAXIMUM_LEVEL 5
static const char *TAG = "app5";
/* ---------- Latency telemetry copied from App 3 ---------- */
static volatile int64_t isr_entry_time_us;
static volatile uint32_t button_presses_observed;
static volatile uint64_t latency_max_notif_us;
static volatile uint64_t latency_max_sem_us;
static volatile bool button_notification_pending;
/* ---------- IPC objects (created in app_main, used everywhere) ---------- */
static QueueHandle_t data_q;
static EventGroupHandle_t evt_group;
static TaskHandle_t responder_handle;
/* Binary semaphore used only for latency comparison against notifications. */
static SemaphoreHandle_t button_sem;
/* Event-group bit definitions */
#define EV_BIT_DATA_PRODUCED (1 << 0)
#define EV_BIT_DATA_PROCESSED (1 << 1)
/* Per-task heartbeats — proof of life for the monitor. Single 32-bit reads are
* atomic on Xtensa, so the monitor can read these without a lock (App 6's topic). */
static volatile uint32_t hb_prod;
static volatile uint32_t hb_cons;
static volatile uint32_t hb_coord;
static volatile uint32_t hb_resp;
/* ---------- Queue item ---------- */
typedef struct {
uint32_t sequence;
uint32_t timestamp_ms;
int threat_level;
int sector;
} defense_report_t;
/* Queue/back-pressure telemetry. */
static volatile uint32_t reports_enqueued;
static volatile uint32_t reports_processed;
static volatile uint32_t reports_dropped;
/* ---------- Producer task (Core 1) ----------
* Generates one perimeter-defense report every 50 ms.
* If the queue stays full for 10 ms, the newest report is dropped.
*/
static void producer_task(void *arg)
{
int tick = 0;
for (;;) {
defense_report_t report = {
.sequence = (uint32_t)tick,
.timestamp_ms = (uint32_t)(esp_timer_get_time() / 1000),
.threat_level = (tick % 5) + 1,
.sector = (tick % 4) + 1
};
BaseType_t sent = xQueueSend(
data_q,
&report,
pdMS_TO_TICKS(10)
);
if (sent == pdPASS) {
reports_enqueued++;
/* Only signal data produced after a successful queue send. */
xEventGroupSetBits(
evt_group,
EV_BIT_DATA_PRODUCED
);
} else {
reports_dropped++;
ESP_LOGW(
TAG,
"[producer] queue full — dropped report #%lu",
(unsigned long)report.sequence
);
}
tick++;
hb_prod++;
vTaskDelay(pdMS_TO_TICKS(50)); /* 20 Hz producer */
}
}
/* ---------- Consumer task (Core 1) ----------
* Receives perimeter-defense reports from the queue and classifies them.
*/
static void consumer_task(void *arg)
{
defense_report_t report;
for (;;) {
BaseType_t received = xQueueReceive(
data_q,
&report,
pdMS_TO_TICKS(250)
);
if (received == pdPASS) {
const char *classification;
if (report.threat_level >= 4) {
classification = "HIGH";
} else if (report.threat_level >= 2) {
classification = "MEDIUM";
} else {
classification = "LOW";
}
/*ESP_LOGI(
TAG,
"[consumer] report #%lu processed: "
"sector=%d threat=%d classification=%s",
(unsigned long)report.sequence,
report.sector,
report.threat_level,
classification
);*/
reports_processed++;
/* Signal only after a report was received and processed. */
xEventGroupSetBits(
evt_group,
EV_BIT_DATA_PROCESSED
);
} else {
/*ESP_LOGW(
TAG,
"[consumer] queue receive timeout"
);*/
}
hb_cons++;
}
}
/* ---------- Coordinator task (Core 1) ----------
* Waits for BOTH event bits to be set, then signals the responder via direct
* task notification.
*/
static void coordinator_task(void *arg)
{
const EventBits_t wait_mask =
EV_BIT_DATA_PRODUCED | EV_BIT_DATA_PROCESSED;
for (;;) {
EventBits_t got = xEventGroupWaitBits(
evt_group,
wait_mask,
pdTRUE, /* clear on exit */
pdTRUE, /* wait for ALL */
portMAX_DELAY
);
if ((got & wait_mask) == wait_mask) {
/*ESP_LOGI(
TAG,
"[coordinator] sensor report produced and "
"threat analysis completed"
);*/
/*
* Leave this commented out while measuring button wake latency.
* Restore it afterward so the normal pipeline notifies the responder.
*/
/* xTaskNotifyGive(responder_handle); */
hb_coord++;
}
}
}
/* ---------- Binary-semaphore bottom-half task: Core 1 ----------
* Measures ISR-to-task wake latency for the semaphore path.
*/
static void button_sem_task(void *arg)
{
for (;;) {
if (xSemaphoreTake(button_sem, portMAX_DELAY) == pdTRUE) {
int64_t wake_time_us = esp_timer_get_time();
int64_t latency_us = wake_time_us - isr_entry_time_us;
if ((uint64_t)latency_us > latency_max_sem_us) {
latency_max_sem_us = (uint64_t)latency_us;
}
ESP_LOGI(
TAG,
"[sem] button press #%lu processed "
"latency=%lld us (max=%llu us)",
(unsigned long)button_presses_observed,
(long long)latency_us,
(unsigned long long)latency_max_sem_us
);
}
}
}
/* ---------- Responder task (Core 1) ----------
* Wakes via direct task notification from coordinator OR from button ISR.
*/
static void responder_task(void *arg)
{
for (;;) {
uint32_t n = ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
if (n == 0) {
continue;
}
/*
* Only calculate ISR-to-task latency when a button interrupt
* generated the notification. Coordinator notifications use the
* same notification counter but do not represent ISR latency.
*/
if (button_notification_pending) {
int64_t wake_time_us = esp_timer_get_time();
int64_t latency_us = wake_time_us - isr_entry_time_us;
button_notification_pending = false;
if ((uint64_t)latency_us > latency_max_notif_us) {
latency_max_notif_us = (uint64_t)latency_us;
}
ESP_LOGI(
TAG,
"[notif] button press #%lu processed "
"latency=%lld us (max=%llu us) "
"notification_count=%lu",
(unsigned long)button_presses_observed,
(long long)latency_us,
(unsigned long long)latency_max_notif_us,
(unsigned long)n
);
} else {
ESP_LOGI(
TAG,
"[responder] coordinator notification received "
"(count=%lu)",
(unsigned long)n
);
}
ESP_LOGI(
TAG,
"[response] perimeter response team dispatched"
);
hb_resp++;
}
}
/* ---------- Button ISR — signal both latency paths ---------- */
static volatile int64_t last_edge_us;
static void IRAM_ATTR button_isr(void *arg)
{
int64_t now = esp_timer_get_time();
if (now - last_edge_us < DEBOUNCE_US) {
return;
}
last_edge_us = now;
/*
* GPIO 19 goes high at accepted ISR entry and low immediately before
* ISR exit. Scope this pin to observe ISR execution duration.
*/
gpio_set_level(ISR_PULSE_GPIO, 1);
isr_entry_time_us = now;
button_presses_observed++;
button_notification_pending = true;
BaseType_t woken = pdFALSE;
#if TEST_SEMAPHORE
xSemaphoreGiveFromISR(
button_sem,
&woken
);
#endif
#if TEST_NOTIFICATION
vTaskNotifyGiveFromISR(
responder_handle,
&woken
);
#endif
gpio_set_level(ISR_PULSE_GPIO, 0);
portYIELD_FROM_ISR(woken);
}
#if USE_WEBSERVER
/* ---------- Web monitor task (Core 0) [USE_WEBSERVER = 1] ----------
* The HTML page is served once. JavaScript polls /state once per second and
* displays:
* - Queue depth
* - Event-group bits
* - Producer, consumer, coordinator, and responder heartbeats
*/
static httpd_handle_t web_server = NULL;
/* ---------- HTTP handler: live JSON state ---------- */
static esp_err_t handle_state(httpd_req_t *req)
{
char buf[256];
UBaseType_t depth =
uxQueueMessagesWaiting(data_q);
EventBits_t bits =
xEventGroupGetBits(evt_group);
int n = snprintf(
buf,
sizeof(buf),
"{"
"\"queue_depth\":%u,"
"\"event_bits\":%u,"
"\"producer\":%lu,"
"\"consumer\":%lu,"
"\"coordinator\":%lu,"
"\"responder\":%lu"
"}",
(unsigned)depth,
(unsigned)bits,
(unsigned long)hb_prod,
(unsigned long)hb_cons,
(unsigned long)hb_coord,
(unsigned long)hb_resp
);
httpd_resp_set_type(
req,
"application/json"
);
httpd_resp_set_hdr(
req,
"Cache-Control",
"no-store"
);
httpd_resp_send(
req,
buf,
n
);
return ESP_OK;
}
/* ---------- HTTP handler: root dashboard page ---------- */
static esp_err_t handle_root(httpd_req_t *req)
{
static const char html[] =
"<!DOCTYPE html>"
"<html lang=\"en\">"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1\">"
"<title>Defense IPC Monitor</title>"
"<style>"
"body{"
"font-family:-apple-system,BlinkMacSystemFont,"
"'Segoe UI',sans-serif;"
"background:#101820;"
"color:#f5f5f5;"
"padding:2rem;"
"margin:0;"
"}"
"h1{"
"color:#ffc904;"
"margin-bottom:.25rem;"
"}"
".subtitle{"
"color:#b8c2cc;"
"margin-top:0;"
"}"
".grid{"
"display:grid;"
"grid-template-columns:"
"repeat(auto-fit,minmax(190px,1fr));"
"gap:1rem;"
"margin-top:2rem;"
"}"
".card{"
"background:#1d2a35;"
"border:1px solid #34495a;"
"border-radius:10px;"
"padding:1rem;"
"}"
".label{"
"color:#b8c2cc;"
"font-size:.85rem;"
"text-transform:uppercase;"
"letter-spacing:.05rem;"
"}"
".value{"
"font-size:2rem;"
"font-weight:700;"
"color:#ffc904;"
"margin-top:.4rem;"
"font-variant-numeric:tabular-nums;"
"}"
".footer{"
"margin-top:2rem;"
"color:#8996a3;"
"font-size:.9rem;"
"}"
"</style>"
"</head>"
"<body>"
"<h1>Defense IPC Monitor</h1>"
"<p class=\"subtitle\">"
"Dual-core producer-consumer pipeline"
"</p>"
"<div class=\"grid\">"
"<div class=\"card\">"
"<div class=\"label\">Queue depth</div>"
"<div id=\"queue\" class=\"value\">--</div>"
"</div>"
"<div class=\"card\">"
"<div class=\"label\">Event-group bits</div>"
"<div id=\"events\" class=\"value\">0x00</div>"
"</div>"
"<div class=\"card\">"
"<div class=\"label\">Producer heartbeat</div>"
"<div id=\"producer\" class=\"value\">--</div>"
"</div>"
"<div class=\"card\">"
"<div class=\"label\">Consumer heartbeat</div>"
"<div id=\"consumer\" class=\"value\">--</div>"
"</div>"
"<div class=\"card\">"
"<div class=\"label\">Coordinator heartbeat</div>"
"<div id=\"coordinator\" class=\"value\">--</div>"
"</div>"
"<div class=\"card\">"
"<div class=\"label\">Responder heartbeat</div>"
"<div id=\"responder\" class=\"value\">--</div>"
"</div>"
"</div>"
"<p class=\"footer\">"
"Polling the <code>/state</code> endpoint once per second."
"</p>"
"<script>"
"async function poll(){"
"try{"
"const r=await fetch('/state',{cache:'no-store'});"
"const s=await r.json();"
"document.getElementById('queue').textContent="
"s.queue_depth;"
"document.getElementById('events').textContent="
"'0x'+s.event_bits.toString(16)"
".padStart(2,'0').toUpperCase();"
"document.getElementById('producer').textContent="
"s.producer;"
"document.getElementById('consumer').textContent="
"s.consumer;"
"document.getElementById('coordinator').textContent="
"s.coordinator;"
"document.getElementById('responder').textContent="
"s.responder;"
"}catch(e){"
"console.log('Monitor update failed',e);"
"}"
"}"
"setInterval(poll,1000);"
"poll();"
"</script>"
"</body>"
"</html>";
httpd_resp_set_type(
req,
"text/html"
);
httpd_resp_send(
req,
html,
HTTPD_RESP_USE_STRLEN
);
return ESP_OK;
}
/* ---------- Start HTTP server ---------- */
static httpd_handle_t start_webserver(void)
{
httpd_config_t cfg =
HTTPD_DEFAULT_CONFIG();
cfg.server_port = HTTP_PORT;
cfg.core_id = PRO_CPU_NUM;
cfg.task_priority = 5;
cfg.stack_size = 8192;
httpd_handle_t server = NULL;
if (httpd_start(&server, &cfg) == ESP_OK) {
httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = handle_root,
.user_ctx = NULL
};
httpd_uri_t state = {
.uri = "/state",
.method = HTTP_GET,
.handler = handle_state,
.user_ctx = NULL
};
httpd_register_uri_handler(
server,
&root
);
httpd_register_uri_handler(
server,
&state
);
ESP_LOGI(
TAG,
"[webmon] HTTP server started on port %d",
HTTP_PORT
);
} else {
ESP_LOGE(
TAG,
"[webmon] HTTP server failed to start"
);
}
return server;
}
/* ---------- Wi-Fi event handler ---------- */
static void wifi_event_handler(
void *arg,
esp_event_base_t base,
int32_t id,
void *data
)
{
if (
base == WIFI_EVENT &&
id == WIFI_EVENT_STA_START
) {
esp_wifi_connect();
} else if (
base == WIFI_EVENT &&
id == WIFI_EVENT_STA_DISCONNECTED
) {
ESP_LOGW(
TAG,
"[webmon] Wi-Fi disconnected; reconnecting"
);
esp_wifi_connect();
} else if (
base == IP_EVENT &&
id == IP_EVENT_STA_GOT_IP
) {
ip_event_got_ip_t *event =
(ip_event_got_ip_t *)data;
ESP_LOGI(
TAG,
"[webmon] Got IP: " IPSTR,
IP2STR(&event->ip_info.ip)
);
if (web_server == NULL) {
web_server = start_webserver();
}
}
}
/* ---------- Initialize Wi-Fi station ---------- */
static void wifi_init_sta(void)
{
esp_err_t ret =
nvs_flash_init();
if (
ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND
) {
ESP_ERROR_CHECK(
nvs_flash_erase()
);
ret =
nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(
esp_event_loop_create_default()
);
esp_netif_create_default_wifi_sta();
wifi_init_config_t init_cfg =
WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(
esp_wifi_init(&init_cfg)
);
ESP_ERROR_CHECK(
esp_event_handler_register(
WIFI_EVENT,
ESP_EVENT_ANY_ID,
wifi_event_handler,
NULL
)
);
ESP_ERROR_CHECK(
esp_event_handler_register(
IP_EVENT,
IP_EVENT_STA_GOT_IP,
wifi_event_handler,
NULL
)
);
wifi_config_t wifi_cfg = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
.threshold.authmode = WIFI_AUTH_OPEN
}
};
ESP_ERROR_CHECK(
esp_wifi_set_mode(WIFI_MODE_STA)
);
ESP_ERROR_CHECK(
esp_wifi_set_config(
WIFI_IF_STA,
&wifi_cfg
)
);
ESP_ERROR_CHECK(
esp_wifi_start()
);
}
/* ---------- Web monitor initialization task ---------- */
static void webmonitor_task(void *arg)
{
ESP_LOGI(
TAG,
"[webmon] initializing Wi-Fi and HTTP monitor"
);
wifi_init_sta();
/* Wi-Fi, the event loop, and HTTP server continue in ESP-IDF tasks. */
vTaskDelete(NULL);
}
#else
/* ---------- Serial monitor task (Core 0) [USE_WEBSERVER = 0] ---------- */
static void serial_monitor_task(void *arg)
{
for (;;) {
UBaseType_t depth =
uxQueueMessagesWaiting(data_q);
EventBits_t bits =
xEventGroupGetBits(evt_group);
ESP_LOGI(
TAG,
"[monitor] q_depth=%u evt=0x%02x "
"hb: prod=%lu cons=%lu coord=%lu resp=%lu",
(unsigned)depth,
(unsigned)bits,
(unsigned long)hb_prod,
(unsigned long)hb_cons,
(unsigned long)hb_coord,
(unsigned long)hb_resp
);
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
#endif /* USE_WEBSERVER */
/* ---------- app_main ---------- */
void app_main(void)
{
esp_log_level_set(
TAG,
ESP_LOG_INFO
);
ESP_LOGI(
TAG,
"==== App 5 Perimeter Defense starting — IPC pipeline ===="
);
#if USE_WEBSERVER
ESP_LOGI(
TAG,
"Monitor: WEB (USE_WEBSERVER=1) — Core-0 HTTP monitor"
);
#else
ESP_LOGI(
TAG,
"Monitor: SERIAL (USE_WEBSERVER=0) — "
"Core-0 summary once/sec, no Wi-Fi"
);
#endif
/*
* Eight reports at a 50 ms production period provide approximately
* 400 ms of buffering.
*/
data_q = xQueueCreate(
8,
sizeof(defense_report_t)
);
evt_group =
xEventGroupCreate();
button_sem =
xSemaphoreCreateBinary();
if (
data_q == NULL ||
evt_group == NULL ||
button_sem == NULL
) {
ESP_LOGE(
TAG,
"Failed to create IPC objects"
);
return;
}
/*
* Create the notification responder first so responder_handle is valid
* before the coordinator or button ISR can use it.
*/
xTaskCreatePinnedToCore(
responder_task,
"response",
4096,
NULL,
12,
&responder_handle,
APP_CPU_NUM
);
/*
* Same priority and core as the responder for a fairer latency comparison.
*/
xTaskCreatePinnedToCore(
button_sem_task,
"btn_sem",
4096,
NULL,
12,
NULL,
APP_CPU_NUM
);
xTaskCreatePinnedToCore(
coordinator_task,
"coord",
4096,
NULL,
9,
NULL,
APP_CPU_NUM
);
xTaskCreatePinnedToCore(
producer_task,
"sensor",
4096,
NULL,
8,
NULL,
APP_CPU_NUM
);
xTaskCreatePinnedToCore(
consumer_task,
"analysis",
4096,
NULL,
8,
NULL,
APP_CPU_NUM
);
/* Observability plane on Core 0. */
#if USE_WEBSERVER
xTaskCreatePinnedToCore(
webmonitor_task,
"webmon",
4096,
NULL,
4,
NULL,
PRO_CPU_NUM
);
#else
xTaskCreatePinnedToCore(
serial_monitor_task,
"monitor",
4096,
NULL,
4,
NULL,
PRO_CPU_NUM
);
#endif
/* Button ISR input. */
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE
};
ESP_ERROR_CHECK(
gpio_config(&cfg)
);
/* GPIO 19 pulse for ISR timing. */
gpio_config_t pulse_cfg = {
.pin_bit_mask = 1ULL << ISR_PULSE_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
ESP_ERROR_CHECK(
gpio_config(&pulse_cfg)
);
ESP_ERROR_CHECK(
gpio_set_level(
ISR_PULSE_GPIO,
0
)
);
ESP_ERROR_CHECK(
gpio_install_isr_service(0)
);
ESP_ERROR_CHECK(
gpio_isr_handler_add(
BUTTON_GPIO,
button_isr,
NULL
)
);
ESP_LOGI(
TAG,
"Press GPIO %d to compare notification and semaphore latency",
BUTTON_GPIO
);
}