/*
* Application 2 — Multi-task system with scheduling defense
*
* Scaffold level: ~70% complete.
*
* Scaffold Code - AI useage:
* Addition of the comment blocks for "real esp32 function" and the "compute standins"
* Logic to allow for switching between webserve mode and pure logging mode
* Commenting of code including human readable summaries
*
* What this scaffold gives you:
* - Architecture from App 1 (dual-core, HTTP server) reused.
* - Four FreeRTOS task skeletons, all pinned to Core 1, with priorities pre-assigned.
* - Per-task heartbeat counters wired into the web page.
* - A WCET measurement helper (MEASURE_WCET) you can wrap around any task body.
*
* What you do:
* 1. Rename the task names and log strings for YOUR theme.
* Tasks are currently named A, B, C, D — give them theme-appropriate names.
* 2. Implement each task's body. Suggested workloads in the comments per task.
* 3. Defend the priority assignment in your README. Use the high-level framework from the slide!
* 4. Measure WCET for each task with MEASURE_WCET. Report mean / max.
* 5. Demonstrate preemption: log a timestamp before/after, show in your README
* that a higher-priority task interrupts a lower-priority one.
*
* What you DON'T need to change:
* - The HTTP server, Wi-Fi setup, or web-page rendering structure.
* - The WCET helper itself — just use it.
* - The xTaskCreatePinnedToCore plumbing.
*
* ============================================================
* OUTPUT MODE (web monitor vs. terminal-only monitor)
* ============================================================
*
* USE_WEBSERVER selects how the live monitor data is surfaced. Both modes
* report the SAME fields (period, priority, heartbeats, WCET-max); only the
* transport differs.
*
* USE_WEBSERVER = 1 -> Wi-Fi + HTTP server, auto-refreshing web page (App 1
* carry-over). Open the printed IP in a browser.
* USE_WEBSERVER = 0 -> No Wi-Fi, no HTTP. A monitor task prints the same
* table to the serial console once per second. Use this
* when you don't want to deal with Wi-Fi/Wokwi-GUEST,
* or want a clean serial trace to paste into your README.
*
* ============================================================
* Theme: Medical Pulse Monitor
* ============================================================
*/
#ifndef USE_WEBSERVER
#define USE_WEBSERVER 0
#endif
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <math.h>
#if USE_WEBSERVER
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#endif
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASS ""
#define CONFIG_LOG_DEFAULT_LEVEL_INFO 1
#define CONFIG_LOG_MAXIMUM_LEVEL 5
static const char *TAG = "app2";
/* ---------- Per-task heartbeat counters (for the monitor) ---------- */
/* Each task increments its counter at the end of every iteration. The monitor
* (web or terminal) reads them and displays values. Single 32-bit reads are
* atomic on Xtensa, so we don't need a mutex around these (yet — App 6 changes
* this). */
static volatile uint32_t hb_a, hb_b, hb_c, hb_d;
/* ---------- WCET measurement helper ----------
*
* Usage:
* uint64_t wcet_a_max_us = 0;
* ...
* MEASURE_WCET(wcet_a_max_us, {
* // your task body code here
* });
*
* The macro records the maximum observed time across all invocations.
* Combine with periodic logging to get the WCET evidence for your README.
*/
#define MEASURE_WCET(_max_var, _body) do { \
int64_t _t0 = esp_timer_get_time(); \
_body; \
int64_t _dt = esp_timer_get_time() - _t0; \
if ((uint64_t)_dt > (_max_var)) (_max_var) = (uint64_t)_dt; \
} while (0)
/* Storage for WCET-max per task. Log these periodically. */
static uint64_t wcet_a_max_us, wcet_b_max_us, wcet_c_max_us, wcet_d_max_us;
/* ============================================================
* PURE-COMPUTE WORKLOAD NOTES (read before filling in the TODOs)
* ============================================================
*
* The suggested bodies below are deliberately PERIPHERAL-FREE. No GPIO,
* esp_random(), flash/NVS reads, etc. The only thing that changes your runtime
* is a tunable constant, which is what you want for your WCET.
*
* Each task's comment now leads with a REAL / WOKWI hardware path (the realworld
* version of the workload) and then lists pure-compute stand-ins. Do the
* hardware path if you can; reach for a stand-in when you want a guaranteed
* deterministic WCET or don't want to wire a part. Again we're early on so
* feel free to just use a drop-in!
*
* Why not just do "random cycles?"
*
* (1) DEAD-CODE ELIMINATION. With optimization on (-O2/-Os), the
* compiler deletes any computation whose result is never observed —
* your whole loop can vanish and report ~0 us. Each kernel ends by
* writing to a `volatile` sink, and seeds itself from that sink, so the
* work is observable and cannot be elided.
*
* (2) INITIALIZE BUFFERS ONCE, NOT IN THE LOOP. malloc()/memset() of large
* buffers inside the period destroys determinism. Declare buffers
* static (file scope or `static` inside the task) and fill them one time
* — e.g. in app_main, or guarded by a `static bool inited` flag.
*
* (3) USE float, NOT double, FOR PREDICTABLE TIMING. The ESP32 FPU is
* single-precision only; `double` is software-emulated and runs ~10-50x
* slower with data-dependent timing. (You CAN use double as a "make it
* slower" knob, but call it out — it's emulated, not free.)
*
* (4) WARM UP. The first invocation pays one-time costs (instruction-cache
* fill from flash, branch predictor cold). Either discard the first
* sample or run a few warm-up iterations before trusting MEASURE_WCET.
*
* (5) WOKWI != SILICON. Constants below are 240 MHz hardware ballpark. Wokwi's
* timing model differs, so MEASURE and tune the *_ITERS / *_N / *_REPS
* knobs until you land in the target band. That tuning IS the assignment.
*
* Utilization sanity check for the README: WCET/period for each task, summed,
* must sit under the rate-monotonic bound (~0.757 for n=4). With the targets
* below you're around 15-20% — comfortably schedulable, which is part of why
* the rate-monotonic priority ordering (higher rate = higher priority) holds.
*/
/* ============================================================
* TASK A priority 15 period 10 ms highest priority
* ============================================================
*
* Suggested workloads per theme:
* Avionics: attitude sensor read (simulate with a counter or ADC read)
* Medical: ECG sample (simulate fast pulse)
* Space: attitude rate sample
* Industrial: dispatch interlock check
* Security: tamper poll
*/
#define A_ITERS 256
static void task_a(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(10);
for (;;) {
MEASURE_WCET(wcet_a_max_us, {
/* TODO(YOU): implement Task A's actual work.
* Something fast — should take well under 1 ms WCET (aim 10-50 us).
*
* KEEP IT TIGHT. This is your highest-priority task; if it runs
* long, it starves everything else.
*
* ----------------------------------------------------------------
* START HERE — REAL / WOKWI HARDWARE PATH (try this first)
* ----------------------------------------------------------------
* The honest version of this task reads a real sensor. In Wokwi the
* cheapest fast sensor is a potentiometer (or photoresistor / NTC)
* wired to an ADC1 pin — a one-shot conversion is a few microseconds,
* which is exactly the "fast highest-rate poll" this slot wants.
* // #include "esp_adc/adc_oneshot.h"
* // // init ONCE (in app_main): adc_oneshot_new_unit + _config_channel
* // int raw;
* // adc_oneshot_read(adc_handle, ADC_CHANNEL_6, &raw); // GPIO34
* // a_latest = raw; // hand off to Task B via a shared var/buffer
* For a GPIO-style poll (interlock/tamper), gpio_get_level() is even
* faster. For an IMU axis, an I2C read (MPU6050) is heavier — that's
* really a Task B workload, not A.
*
* Theme fit for the hardware path:
* Avionics -> pot = control-surface / attitude angle proxy
* Medical -> pot/photoresistor = analog ECG front-end sample
* Space -> pot = single attitude-rate (gyro) axis proxy
* Industrial -> gpio_get_level() = dispatch interlock switch
* Security -> gpio_get_level() = tamper line poll
*
* ----------------------------------------------------------------
* PURE-COMPUTE STAND-INS (no peripheral; pick one, tune the knob)
* ----------------------------------------------------------------
* Use these if you'd rather not wire a part, or want a guaranteed
* deterministic WCET. They stand in for the sensor read + a cheap
* post-process. Tune *_ITERS against MEASURE_WCET.
*
* OPTION 1 — xorshift32 PRNG churn (integer, ~5-6 cyc/iter).
* Fits Security (tamper-entropy / nonce churn) and any theme that
* just needs a tight, predictable integer burn.
* // #define A_ITERS 300 // ~tens of us; raise to lengthen
* // static volatile uint32_t a_sink; // <- defeats DCE
* // uint32_t x = a_sink ? a_sink : 0xACE1u; // seed from sink (observable)
* // for (int i = 0; i < A_ITERS; i++) {
* // x ^= x << 13; x ^= x >> 17; x ^= x << 5; // xorshift32
* // }
* // a_sink = x;
*
* OPTION 2 — fixed-point IIR (single-pole EMA) on a synthetic input.
* "Filter one sensor reading," Q16.16 math. Fits Avionics / Medical /
* Space / Industrial — anything where the real path is "sample, then
* smooth it" before handing off.
*/
static volatile int32_t a_sink;
int32_t y = a_sink; // state, seeded from sink
const int32_t alpha = 6553; // ~0.1 in Q16.16
for (int i = 0; i < A_ITERS; i++) {
int32_t in = (i * 1103515245 + 12345); // cheap synthetic sample
y += (int32_t)(((int64_t)alpha * (in - y)) >> 16);
}
a_sink = y;
// Log when Task A runs to prove it interrupts Task D
if (hb_a % 10 == 0) {
ESP_LOGI(TAG, "[Task A] tick t=%lld", esp_timer_get_time());
}
});
hb_a++;
vTaskDelayUntil(&last, period);
}
}
/* ============================================================
* TASK B priority 10 period 20 ms
* ============================================================
*
* Suggested workloads:
* Avionics: inner-loop control update
* Medical: arrhythmia detection
* Space: attitude update / fusion
* Industrial: motor speed control
* Security: attestation digest
*/
#define B_WIN 1024
static void task_b(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(20);
for (;;) {
MEASURE_WCET(wcet_b_max_us, {
/* TODO(YOU): Task B's body. Target: a few hundred microseconds.
*
* ----------------------------------------------------------------
* START HERE — REAL / WOKWI HARDWARE PATH (hard, but you might try this first)
* ----------------------------------------------------------------
* B is the "process what A sampled" stage: a control law or a
* detector running on the buffer A fills. The realistic Wokwi move
* is either (a) consume A's shared sample window in software, or
* (b) pull a richer sensor that needs a real bus transaction — an
* MPU6050 IMU over I2C is the canonical one (multi-byte read, tens
* of us), and its 6-axis output is what a fusion/control loop wants.
* // #include "driver/i2c_master.h"
* // // init I2C ONCE in app_main; then per period:
* // uint8_t reg = 0x3B, rx[14]; // ACCEL_XOUT_H..GYRO_ZOUT_L
* // i2c_master_transmit_receive(dev, ®, 1, rx, sizeof(rx), -1);
* // // unpack rx -> ax,ay,az,gx,gy,gz, run your control/detect step
* If you drive an actuator, an LEDC PWM update (servo / motor) is the
* output side of this loop.
*
* Theme fit for the hardware path:
* Avionics -> inner-loop PID on attitude error (IMU in, PWM out)
* Medical -> arrhythmia detector over the ECG sample window
* Space -> accel+gyro complementary fusion (attitude update)
* Industrial -> motor-speed PID, drive servo via LEDC
* Security -> attestation digest over a memory region (see Opt 2)
*
* ----------------------------------------------------------------
* PURE-COMPUTE STAND-INS (no peripheral; pick one, tune the knob)
* ----------------------------------------------------------------
*
* OPTION 1 — single-precision FIR convolution. The general-purpose
* "filter/detect on a window" kernel. Runtime scales as
* B_SAMP * B_TAPS MACs; 256*128 = 32768 ~= 150-250 us.
* Fits Avionics (control filter), Medical (matched-filter style
* arrhythmia detect), Industrial (smoothed speed estimate).
* // #define B_SAMP 256
* // #define B_TAPS 128 // <= B_SAMP
* // static float b_buf[B_SAMP]; // fill once in app_main
* // static float b_coef[B_TAPS]; // fill once in app_main
* // static volatile float b_sink;
* // float acc = b_sink; // seed from sink (observable)
* // for (int n = 0; n < B_SAMP; n++)
* // for (int k = 0; k < B_TAPS; k++)
* // acc += b_buf[(n + B_SAMP - k) & (B_SAMP - 1)] * b_coef[k];
* // // index stays >= 0; B_SAMP must be a power of two for the mask
* // b_sink = acc;
*
* OPTION 2 — windowed statistics with sqrtf (mean/variance/std, then
* a threshold compare; the sqrtf is the "decision"). Fits Medical
* (beat-to-beat variability), Space (sensor-health/outlier check),
* Security (entropy/variance estimate over a sampled region).
*/
static float b_win[B_WIN]; // fill once
static volatile float b_sink;
float mean = 0.0f; for (int i=0;i<B_WIN;i++) mean += b_win[i];
mean /= B_WIN;
float var = 0.0f;
for (int i=0;i<B_WIN;i++){ float d=b_win[i]-mean; var += d*d; }
b_sink = sqrtf(var / B_WIN); // std dev -> "detector"
// MEDICAL THEME: Windowed statistics (std dev) for Arrhythmia Detection
});
hb_b++;
vTaskDelayUntil(&last, period);
}
}
/* ============================================================
* TASK C priority 5 period 50 ms
* ============================================================
*
* Suggested workloads:
* Avionics: telemetry packet assembly
* Medical: alarm dispatch
* Space: downlink prep
* Industrial: operator-display refresh
* Security: audit log entry assembly
*/
#define C_LEN 256
static void task_c(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(40);
for (;;) {
MEASURE_WCET(wcet_c_max_us, {
/* TODO(YOU): Task C's body. Target: 1-5 ms.
*
* ----------------------------------------------------------------
* REAL / WOKWI HARDWARE PATH (skip this go to STAND-INS for sure!)
* ----------------------------------------------------------------
* C is the periodic "make the results presentable / shippable" stage:
* assemble a frame, stamp it, push it out. The natural Wokwi part is
* an SSD1306 OLED over I2C — redrawing a status screen every 50 ms is
* a real, visibly-bursty workload, and an I2C frame flush to the panel
* is genuinely in the low-ms range, which matches this slot.
* // #include "ssd1306.h" // or your I2C display driver
* // ssd1306_clear(&dev);
* // ssd1306_draw_string(&dev, 0, 0, "A=%lu B=%lu", hb_a, hb_b);
* // ssd1306_refresh(&dev); // <-- the I2C burst you're timing
* No display? Build the telemetry buffer in RAM and CRC-stamp it
* (that's Option 2 below — same shape, no peripheral).
*
* Theme fit for the hardware path:
* Avionics -> telemetry packet assembly + CRC, then radio/UART out
* Medical -> alarm dispatch: evaluate thresholds, format + show
* Space -> downlink frame prep (pack fields, add CRC/parity)
* Industrial -> operator-display refresh on the SSD1306 OLED
* Security -> audit-log entry assembly (hash-chained record)
*
* ----------------------------------------------------------------
* PURE-COMPUTE STAND-INS (no peripheral; pick one, tune the knob)
* ----------------------------------------------------------------
*
* OPTION 1 — N x N float matrix multiply, repeated. The "fusion /
* estimator" kernel; cubic in C_N so easy to push into 1-5 ms.
* 48^3 * 4 ~= 442k MACs ~= ~2 ms. Memory = 3 * C_N^2 * 4 (~27 KB at N=48).
* Fits Space (state estimation / covariance update), Avionics
* (sensor fusion), Industrial (model-based estimator).
* // #define C_N 48
* // #define C_REPS 4
* // static float cA[C_N*C_N], cB[C_N*C_N], cC[C_N*C_N]; // fill once
* // static volatile float c_sink;
* // cA[0] += c_sink; // touch sink so init isn't elided
* // for (int r = 0; r < C_REPS; r++)
* // for (int i = 0; i < C_N; i++)
* // for (int j = 0; j < C_N; j++) {
* // float acc = 0.0f;
* // for (int k = 0; k < C_N; k++) acc += cA[i*C_N+k] * cB[k*C_N+j];
* // cC[i*C_N+j] = acc;
* // }
* // c_sink = cC[C_N*C_N - 1];
*
* OPTION 2 — CRC-32 over a buffer (integrity stamp on a "telemetry
* packet"). Pure integer, very deterministic; tune by buffer size
* (~32-64 KB -> ~1-3 ms). This IS the no-display version of the
* hardware path. Fits Avionics / Space (frame CRC before downlink)
* and Security (audit-record / hash-chain stamp).
*/
// bytes
static uint8_t c_pkt[C_LEN]; // fill once
static volatile uint32_t c_sink;
uint32_t crc = 0xFFFFFFFFu ^ c_sink; // seed from sink
for (int n = 0; n < C_LEN; n++) {
crc ^= c_pkt[n];
for (int b = 0; b < 8; b++)
crc = (crc >> 1) ^ (0xEDB88320u & (-(int32_t)(crc & 1)));
}
c_sink = crc ^ 0xFFFFFFFFu;
// MEDICAL THEME: CRC-32 integrity stamp on Alarm Telemetry Packet
});
hb_c++;
vTaskDelayUntil(&last, period);
}
}
/* ============================================================
* TASK D priority 2 period 100 ms lowest priority
* ============================================================
*
* Suggested workloads:
* All themes: housekeeping / logging.
*
* This task is intentionally interruptible. If A/B/C take longer than expected,
* D's deadline can slip. That's by design — you'll defend this trade-off
* in your README. Its length also makes it the obvious target for your
* preemption demo: log a timestamp before/after and you'll see A/B/C cut in.
*/
#define D_N 1500
static void task_d(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(100);
for (;;) {
MEASURE_WCET(wcet_d_max_us, {
/* TODO(YOU): Task D's body — slow housekeeping. Deliberately long.
*
* ----------------------------------------------------------------
* SKIP THIS — REAL / WOKWI HARDWARE PATH (GO TO PURE COMPUTE!)
* ----------------------------------------------------------------
* D is background upkeep: heartbeat indicator, slow logging, periodic
* self-test. The Wokwi-friendly version toggles a status LED or a
* NeoPixel and writes a serial summary — cheap, but a real self-test
* (recompute a checksum over a config region, re-derive running stats)
* is what makes D long enough to be the one that gets preempted.
* // gpio_set_level(STATUS_LED, hb_d & 1); // heartbeat blink
* // ESP_LOGI(TAG, "hk: hbA=%lu hbB=%lu ...", hb_a, hb_b);
* // // optional: periodic integrity self-test (heavier; see options)
* Across ALL themes this is the same role — housekeeping/logging/
* watchdog-feed — which is why the original header lists no per-theme
* split for D. The theme flavor is just *what* you log or self-test.
*
* The compute options below stand in for that periodic self-test:
* each is deliberately heavy so A/B/C visibly cut in (your preemption
* demo). Reverse-sorted insertion sort also gives a clean WCET story.
*
* ----------------------------------------------------------------
* PURE-COMPUTE STAND-INS [If needed] (no peripheral; pick one, tune the knob)
* ----------------------------------------------------------------
*
* OPTION 1 — insertion sort, FORCED worst case.
* Reset the array to reverse-sorted EVERY iteration so the O(n^2)
* path fires every period -> your measured max is the TRUE WCET,
* not a data-dependent fluke. (Skip the reset and after the first
* sort the array is sorted -> O(n) best case -> misleading WCET.)
* n=1500 reverse ~= 1.1M compare-shifts ~= several ms. Theme flavor:
* "sort the pending event/alarm queue" for any theme.
* // #define D_N 1500
* // static int d_arr[D_N];
* // static volatile int d_sink;
* // for (int i = 0; i < D_N; i++) d_arr[i] = D_N - i + (d_sink & 1);
* // for (int i = 1; i < D_N; i++) { // insertion sort
* // int key = d_arr[i], j = i - 1;
* // while (j >= 0 && d_arr[j] > key) { d_arr[j+1] = d_arr[j]; j--; }
* // d_arr[j+1] = key;
* // }
* // d_sink = d_arr[D_N/2];
*
* OPTION 2 — Sieve of Eratosthenes, count primes < D_LIM. Integer,
* deterministic, ~ D_LIM*log log D_LIM; D_LIM ~ 20000-50000 -> low
* single-digit ms. Theme flavor: a periodic "integrity sweep" /
* batch table rebuild (fits Security and Industrial self-test).
* // #define D_LIM 30000
* // static uint8_t is_comp[D_LIM]; // 0=prime, 1=composite
* // static volatile uint32_t d_sink;
* // for (int i = 0; i < D_LIM; i++) is_comp[i] = 0;
* // uint32_t count = d_sink & 0; // observable zero seed
* // for (int p = 2; (long)p * p < D_LIM; p++)
* // if (!is_comp[p])
* // for (int m = p*p; m < D_LIM; m += p) is_comp[m] = 1;
* // for (int i = 2; i < D_LIM; i++) count += !is_comp[i];
* // d_sink = count;
*
* OPTION 3 — Leibniz/Nilakantha pi series. Each term is a float
* DIVISION (multi-cycle), so it burns time fast per term; D_TERMS ~
* 200000+ -> several ms. The "dial it to any duration" knob — handy
* if you just need D long enough to make the preemption demo obvious.
* // #define D_TERMS 300000
* // static volatile float d_sink;
* // float pi = d_sink * 0.0f, sign = 1.0f;
* // for (int k = 0; k < D_TERMS; k++) {
* // pi += sign / (float)(2*k + 1);
* // sign = -sign;
* // }
* // d_sink = pi * 4.0f;
*
* BONUS: log the WCET-max for all four tasks every iteration:
* ESP_LOGI(TAG, "WCET us A=%llu B=%llu C=%llu D=%llu",
* wcet_a_max_us, wcet_b_max_us, wcet_c_max_us, wcet_d_max_us);
* This gives you a serial trace you can paste into the README. */
// Log start of housekeeping
ESP_LOGI(TAG, "[Task D] Log Start: %lld us", esp_timer_get_time());
// MEDICAL THEME: Housekeeping - sorting pending alarm/event queue
static int d_arr[D_N];
static volatile int d_sink;
for (int i = 0; i < D_N; i++) d_arr[i] = D_N - i + (d_sink & 1);
for (int i = 1; i < D_N; i++) {
int key = d_arr[i];
int j = i - 1;
while (j >= 0 && d_arr[j] > key) { d_arr[j+1] = d_arr[j]; j--; }
d_arr[j+1] = key;
}
d_sink = d_arr[D_N/2];
// Log end of housekeeping
ESP_LOGI(TAG, "[Task D] Log End: %lld us", esp_timer_get_time());
});
hb_d++;
vTaskDelayUntil(&last, period);
}
}
#if USE_WEBSERVER
/* ============================================================
* WEB MONITOR (USE_WEBSERVER = 1)
* ============================================================ */
/* ---------- HTTP handler: live status page ---------- */
static esp_err_t handle_root(httpd_req_t *req)
{
/* Buffer sized comfortably above worst-case rendered HTML +
* widest possible %lu / %llu substitutions. */
char buf[2048];
int n = snprintf(buf, sizeof(buf),
"<!DOCTYPE html>"
"<html lang=\"en\"><head>"
"<meta charset=\"utf-8\"><meta http-equiv=\"refresh\" content=\"1\">"
"<title>Medical Pulse Monitor · 4-task monitor</title>"
"<style>"
" body { font-family: -apple-system, sans-serif; background: #FAFAF5; "
" color: #1A1A1A; padding: 1.5rem; }"
" h1 { color: #6B4F09; border-bottom: 3px solid #FFC904; "
" display: inline-block; padding-bottom: 4px; }"
" table { border-collapse: collapse; margin: 1rem 0; }"
" th { background: #1A1A1A; color: #FFC904; padding: 8px 14px; "
" text-align: left; font-size: 12px; text-transform: uppercase; }"
" td { padding: 6px 14px; border-bottom: 1px solid #ddd; }"
" td.num { font-variant-numeric: tabular-nums; font-weight: 700; "
" color: #6B4F09; }"
"</style></head>"
"<body>"
"<h1>Medical Pulse Monitor · 4-task monitor</h1>"
"<table>"
"<thead><tr><th>Task</th><th>Period</th><th>Priority</th>"
"<th>Heartbeats</th><th>WCET (µs)</th></tr></thead>"
"<tbody>"
"<tr><td>A</td><td>10 ms</td><td>15</td>"
"<td class=\"num\">%lu</td><td class=\"num\">%llu</td></tr>"
"<tr><td>B</td><td>20 ms</td><td>10</td>"
"<td class=\"num\">%lu</td><td class=\"num\">%llu</td></tr>"
"<tr><td>C</td><td>50 ms</td><td>5</td>"
"<td class=\"num\">%lu</td><td class=\"num\">%llu</td></tr>"
"<tr><td>D</td><td>100 ms</td><td>2</td>"
"<td class=\"num\">%lu</td><td class=\"num\">%llu</td></tr>"
"</tbody></table>"
"<p>Auto-refresh 1 s. Heartbeats should grow monotonically; if one "
"stops, that task is starved or hung.</p>"
"</body></html>",
(unsigned long)hb_a, (unsigned long long)wcet_a_max_us,
(unsigned long)hb_b, (unsigned long long)wcet_b_max_us,
(unsigned long)hb_c, (unsigned long long)wcet_c_max_us,
(unsigned long)hb_d, (unsigned long long)wcet_d_max_us);
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, buf, n);
return ESP_OK;
}
/* ---------- Boilerplate: Wi-Fi + HTTP server (carried from App 1) ---------- */
static httpd_handle_t start_webserver(void)
{
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.server_port = 80;
cfg.core_id = 0;
cfg.task_priority = 5;
cfg.stack_size = 8192;
httpd_handle_t s = NULL;
if (httpd_start(&s, &cfg) == ESP_OK) {
httpd_uri_t root = { .uri="/", .method=HTTP_GET, .handler=handle_root, .user_ctx=NULL };
httpd_register_uri_handler(s, &root);
}
return s;
}
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_wifi_connect();
else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *e = (ip_event_got_ip_t *)data;
ESP_LOGI(TAG, "IP: " IPSTR, IP2STR(&e->ip_info.ip));
start_webserver();
}
}
static void wifi_init_sta(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
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 = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&init));
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL);
wifi_config_t 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, &cfg));
ESP_ERROR_CHECK(esp_wifi_start());
}
#else /* USE_WEBSERVER == 0 */
/* ============================================================
* TERMINAL MONITOR (USE_WEBSERVER = 0)
* ============================================================
*
* Same data, no network. A dedicated task prints the monitor table to the
* serial console once per second (mirrors the web page's 1 s auto-refresh).
*
* Notes:
* - Pinned to Core 0 (PRO_CPU_NUM) so it stays OFF Core 1 with the real-time
* workload tasks — same isolation the HTTP server gave you, so the monitor
* never perturbs your WCET numbers.
* - Uses printf (not ESP_LOGI) so the table prints clean, without a per-line
* log prefix. Swap to ESP_LOGI if you'd rather have timestamps on each row.
* - The Period/Priority columns show the SAME intended values as the web page
* (rate-monotonic targets). They are display constants here, just as in the
* HTML — change them alongside your xTaskCreate priorities to keep the
* monitor honest once you assign real priorities.
*/
static void task_monitor(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(1000);
for (;;) {
printf("\n=== Medical Pulse Monitor · 4-task monitor ===\n");
printf("%-5s %-8s %-9s %-12s %-10s\n",
"Task", "Period", "Priority", "Heartbeats", "WCET(us)");
printf("%-5s %-8s %-9d %-12lu %-10llu\n",
"A", "10 ms", 15, (unsigned long)hb_a, (unsigned long long)wcet_a_max_us);
printf("%-5s %-8s %-9d %-12lu %-10llu\n",
"B", "20 ms", 10, (unsigned long)hb_b, (unsigned long long)wcet_b_max_us);
printf("%-5s %-8s %-9d %-12lu %-10llu\n",
"C", "50 ms", 5, (unsigned long)hb_c, (unsigned long long)wcet_c_max_us);
printf("%-5s %-8s %-9d %-12lu %-10llu\n",
"D", "100 ms", 2, (unsigned long)hb_d, (unsigned long long)wcet_d_max_us);
printf("(heartbeats should grow monotonically; a stalled counter = "
"starved or hung task)\n");
vTaskDelayUntil(&last, period);
}
}
#endif /* USE_WEBSERVER */
/* ---------- app_main ---------- */
void app_main(void)
{
esp_log_level_set(TAG, ESP_LOG_INFO);
ESP_LOGI(TAG, "==== App 2 [Medical Pulse Monitor] starting — 4-task scheduler demo ====");
#if USE_WEBSERVER
ESP_LOGI(TAG, "Output mode: WEB MONITOR (USE_WEBSERVER=1) — open the printed IP");
wifi_init_sta();
#else
ESP_LOGI(TAG, "Output mode: TERMINAL MONITOR (USE_WEBSERVER=0) — no Wi-Fi, serial only");
/* Monitor on Core 0 to mirror the HTTP server's isolation from Core 1. */
xTaskCreatePinnedToCore(task_monitor, "task_monitor", 4096, NULL, 1, NULL, PRO_CPU_NUM);
#endif
/* Create the four tasks. Pinned to Core 1 to isolate from Wi-Fi on Core 0.
* Priority assignment IS YOURS TO CHOOSE (all = 1) — discuss in your README why these
* priorities make sense for these periods
* hint: higher rate = higher priority? Also, see slide deck!. */
xTaskCreatePinnedToCore(task_a, "task_a", 2048, NULL, 15, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(task_b, "task_b", 2048, NULL, 10, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(task_c, "task_c", 2048, NULL, 5, NULL, APP_CPU_NUM);
xTaskCreatePinnedToCore(task_d, "task_d", 2048, NULL, 2, NULL, APP_CPU_NUM);
}