/*
* 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 — the stub you
* fill in with App 1's HTTP server (deliverable #3). 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: YOURTHEME
* ============================================================
*/
#ifndef USE_WEBSERVER
#define USE_WEBSERVER 0
#endif
#include <stdio.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 BUTTON_GPIO GPIO_NUM_18
#define CONFIG_LOG_DEFAULT_LEVEL_INFO 1
#define CONFIG_LOG_MAXIMUM_LEVEL 5
static const char *TAG = "app5";
/* ---------- IPC objects (created in app_main, used everywhere) ---------- */
static QueueHandle_t data_q; /* TODO: choose depth + item size for YOUR pipeline */
static EventGroupHandle_t evt_group;
static TaskHandle_t responder_handle;
/* 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, hb_cons, hb_coord, hb_resp;
/* ---------- Producer task (Core 1) ----------
* TODO(YOU): generate themed data. Push it into data_q. Set EV_BIT_DATA_PRODUCED.
*/
static void producer_task(void *arg)
{
int tick = 0;
for (;;) {
/* TODO: build a themed data item.
* Suggested struct: { uint32_t timestamp_ms; int value; } */
/* TODO: push into queue with a timeout. Decide back-pressure policy. */
xEventGroupSetBits(evt_group, EV_BIT_DATA_PRODUCED);
tick++;
hb_prod++;
vTaskDelay(pdMS_TO_TICKS(50)); /* 20 Hz producer */
}
}
/* ---------- Consumer task (Core 1) ----------
* TODO(YOU): pop from queue, process, set EV_BIT_DATA_PROCESSED. */
static void consumer_task(void *arg)
{
for (;;) {
/* TODO(YOU): replace this placeholder wait with a real
* xQueueReceive(data_q, &item, timeout) plus themed processing.
* The delay below only keeps the scaffold from busy-spinning (and
* starving Core 1's idle task) until you wire the queue up — a real
* xQueueReceive blocks on its own, so delete this line when you add it. */
vTaskDelay(pdMS_TO_TICKS(50));
/* TODO: process the item (themed work). */
xEventGroupSetBits(evt_group, EV_BIT_DATA_PROCESSED);
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) {
/* TODO: do whatever the "cycle complete" event means for your theme.
* Then notify the responder. */
// Just getting started and want to see your button presses?
// Comment out the line below. -mb
xTaskNotifyGive(responder_handle);
hb_coord++;
}
}
}
/* ---------- 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;
ESP_LOGI(TAG, "[responder] notified (count=%lu)", (unsigned long)n);
/* TODO: themed action. */
hb_resp++;
}
}
/* ---------- Button ISR — notify responder directly ---------- */
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 < 200) return;
last_edge_us = now;
BaseType_t woken = pdFALSE;
vTaskNotifyGiveFromISR(responder_handle, &woken);
portYIELD_FROM_ISR(woken);
}
#if USE_WEBSERVER
/* ---------- Web monitor task (Core 0) [USE_WEBSERVER = 1] ----------
* TODO(YOU) — deliverable #3. Port the HTTP server from App 1/2 here:
* - nvs_flash_init(); esp_netif_init(); esp_event_loop_create_default();
* - wifi_init_sta(); (STA connect — serial prints "Got IP: 10.13.37.x")
* - start_webserver(); register a root handler that renders:
* * uxQueueMessagesWaiting(data_q) — live queue depth
* * xEventGroupGetBits(evt_group) — event-group bit state
* * hb_prod / hb_cons / hb_coord / hb_resp — per-task heartbeats
* The Wi-Fi headers are already included above under USE_WEBSERVER, and the
* Wi-Fi/HTTP components are in this folder's CMakeLists REQUIRES.
* Auto-refresh ~1 Hz; faster and your handler shows up in latency measurements.
*/
static void webmonitor_task(void *arg)
{
/* TODO: implement (copy App 1's wifi_init_sta / handle_root / start_webserver). */
ESP_LOGI(TAG, "[webmon] stub — implement the HTTP server here (USE_WEBSERVER=1)");
for (;;) vTaskDelay(pdMS_TO_TICKS(1000));
}
#else
/* ---------- Serial monitor task (Core 0) [USE_WEBSERVER = 0] ----------
* Provided and working. Prints the same state the web monitor will show, so the
* pipeline is observable in Wokwi with no Wi-Fi. This is your baseline; the web
* monitor (USE_WEBSERVER=1) renders the identical fields over HTTP.
*/
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 [YOURTHEME] starting — IPC pipeline ====");
#if USE_WEBSERVER
ESP_LOGI(TAG, "Monitor: WEB (USE_WEBSERVER=1) — implement webmonitor_task (Core 0)");
#else
ESP_LOGI(TAG, "Monitor: SERIAL (USE_WEBSERVER=0) — Core-0 summary once/sec, no Wi-Fi");
#endif
/* TODO: pick queue length + item size. Defend in README.
* Hint: producer at 20 Hz, consumer at unknown rate — what burst size? */
data_q = xQueueCreate(/*depth=*/ 8, /*item size=*/ sizeof(int));
evt_group = xEventGroupCreate();
/* Tasks on Core 1 (real-time plane). 4096-byte stacks: any task that calls
* ESP_LOGI needs headroom for the vprintf formatting (2048 overflows). */
xTaskCreatePinnedToCore(producer_task, "prod", 4096, NULL, 8, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(consumer_task, "cons", 4096, NULL, 8, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(coordinator_task, "coord", 4096, NULL, 9, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(responder_task, "resp", 4096, NULL, 12, &responder_handle, APP_CPU_NUM);
/* Observability plane on Core 0 (networking plane) */
#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 */
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << BUTTON_GPIO, .mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, .intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(BUTTON_GPIO, button_isr, NULL);
}