2025-10-20 08:31:20 +03:00

623 lines
22 KiB
C

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "driver/uart_vfs.h"
#include "esp_err.h"
#include "esp_vfs_dev.h"
#include "esp_system.h"
#include "driver/ledc.h"
/*======== CONFIG ========*/
// Blink defaults
static uint32_t g_blink_delay_ms = 100;
// Console config
#define CLI_LINE_MAX 128
#define HIST_MAX 10
// PWM configuration (safe defaults)
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_8_BIT // 0..(2^RES-1)
#define LEDC_FREQUENCY_DEFAULT 5000 // 5 kHz
/*======== SAFE GPIO CANDIDATES ========*/
static const gpio_num_t TEST_SAFE_GPIO[] = {
1, 2,
4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18,
/* 19,20 excluded (USB D-/D+) */
21,
/* 22..34 not bonded on typical modules */
/* 35,36,37 excluded (PSRAM on some variants) */
38, 39, 40, 41, 42, /* JTAG-capable if configured */
/* 43,44 excluded (UART0 console) */
47, 48 /* may be 1.8 V on R16V */
};
static const size_t TEST_SAFE_GPIO_COUNT = sizeof(TEST_SAFE_GPIO)/sizeof(TEST_SAFE_GPIO[0]);
/*======== CONSTANTS & HELPERS ========*/
/* Sentinels */
#define HIST_NONE (-1)
#define INVALID_GPIO ((gpio_num_t)-1)
/* Bounds */
#define BLINK_DELAY_MIN_MS 10u
#define BLINK_DELAY_MAX_MS 60000u
#define BRIGHT_MIN 0
#define BRIGHT_MAX 99
#define FREQ_MIN_HZ 100u
#define FREQ_MAX_HZ 50000u /* conservative for 8-bit @ low-speed */
/* Task sizes & priorities */
#define BLINK_STACK 2048
#define CONSOLE_STACK 4096
#define BLINK_TASK_PRIO (tskIDLE_PRIORITY + 1)
#define CONSOLE_TASK_PRIO (tskIDLE_PRIORITY + 2)
/* UART driver buffer */
#define UART_RX_BUF 512
/* Generic clamps */
static inline uint32_t clamp_u32(uint32_t v, uint32_t lo, uint32_t hi) {
return (v < lo) ? lo : (v > hi) ? hi : v;
}
static inline int clamp_int(int v, int lo, int hi) {
return (v < lo) ? lo : (v > hi) ? hi : v;
}
/* LEDC helpers */
static inline uint32_t ledc_max_duty(void) {
// LEDC_DUTY_RES is enum like LEDC_TIMER_8_BIT → 8
return (1u << LEDC_DUTY_RES) - 1u;
}
static inline uint32_t brightness_to_duty(uint8_t level) {
// Map BRIGHT_MIN..BRIGHT_MAX → 0..max_duty; guard divide
const uint32_t maxd = ledc_max_duty();
const uint32_t denom = (BRIGHT_MAX > 0) ? (uint32_t)BRIGHT_MAX : 1u;
return (uint32_t)level * maxd / denom;
}
/* Tiny predicates */
static inline bool is_valid_gpio(gpio_num_t pin) { return pin != INVALID_GPIO; }
/*======== RUNTIME ========*/
static gpio_num_t g_current_pin = INVALID_GPIO;
static bool g_blinking = false;
static bool g_pwm_mode = false;
static uint8_t g_pwm_brightness = 50; // BRIGHT_MIN..BRIGHT_MAX
static uint32_t g_pwm_frequency = LEDC_FREQUENCY_DEFAULT;
static TaskHandle_t g_blink_task = NULL;
/*======== CLI ========*/
typedef struct {
uart_port_t uart;
char *line;
size_t *pos;
size_t *cursor;
char (*hist)[CLI_LINE_MAX];
int *hist_count, *hist_head, *hist_view;
} cli_ctx_t;
/*---- CLI helpers ----*/
static inline void cli_write(cli_ctx_t *c, const char *s) {
uart_write_bytes(c->uart, s, strlen(s));
}
static void cli_prompt(cli_ctx_t *c) {
cli_write(c, "\r\n> ");
fflush(stdout);
}
static void cli_redraw(cli_ctx_t *c) {
// Clear line, print prompt + buffer, then move cursor left if needed
cli_write(c, "\r\x1b[2K> ");
if (*c->pos) uart_write_bytes(c->uart, c->line, *c->pos);
if (*c->pos > *c->cursor) {
char seq[16];
int n = snprintf(seq, sizeof(seq), "\x1b[%zuD", (size_t)(*c->pos - *c->cursor));
uart_write_bytes(c->uart, seq, n);
}
}
static void cli_push_hist(cli_ctx_t *c, const char *cmd) {
if (!cmd[0]) return;
int last = (*c->hist_head - 1 + HIST_MAX) % HIST_MAX;
if (*c->hist_count > 0 && strncmp(c->hist[last], cmd, CLI_LINE_MAX) == 0) return;
strncpy(c->hist[*c->hist_head], cmd, CLI_LINE_MAX - 1);
c->hist[*c->hist_head][CLI_LINE_MAX - 1] = 0;
*c->hist_head = (*c->hist_head + 1) % HIST_MAX;
if (*c->hist_count < HIST_MAX) (*c->hist_count)++;
}
static void cli_load_hist(cli_ctx_t *c, int idx) {
strncpy(c->line, c->hist[idx], CLI_LINE_MAX - 1);
c->line[CLI_LINE_MAX - 1] = 0;
*c->pos = *c->cursor = strlen(c->line);
cli_redraw(c);
}
static inline bool cli_in_hist(cli_ctx_t *c) { return *c->hist_view != HIST_NONE; }
/*======== HELPERS ========*/
static bool is_test_safe(gpio_num_t gpio) {
for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
if (TEST_SAFE_GPIO[i] == gpio) return true;
}
return false;
}
static void print_test_safe_pins(void) {
printf("\nTest-safe GPIOs (ESP32-S3):\n ");
for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
printf("%s%d", (i ? ", " : ""), TEST_SAFE_GPIO[i]);
}
printf("\nExcluded (reason): 0,3,45,46 (boot/strap/JTAG) | 19,20 (USB D-/D+) | 35-37 (PSRAM on some) | 43,44 (UART0 console)\n");
printf("Note: On some R16V modules, GPIO47/48 are 1.8 V only.\n");
}
static void release_pin(gpio_num_t pin) {
if (is_valid_gpio(pin)) {
gpio_set_level(pin, 0);
gpio_reset_pin(pin); // back to default (input/hi-z)
}
}
static void stop_pwm(void) {
if (g_pwm_mode) {
ledc_stop(LEDC_MODE, LEDC_CHANNEL, 0);
g_pwm_mode = false;
}
}
static void stop_blink(void) {
if (g_blink_task) {
TaskHandle_t t = g_blink_task;
g_blink_task = NULL; // signal task to exit
for (int i = 0; i < 20 && eTaskGetState(t) != eDeleted; ++i) {
vTaskDelay(pdMS_TO_TICKS(5));
}
}
g_blinking = false;
}
static void stop_all(void) {
stop_blink();
stop_pwm();
release_pin(g_current_pin);
g_current_pin = INVALID_GPIO;
printf("Stopped. Pin released to INPUT.\n");
}
/*======== BLINK TASK ========*/
static void blink_task(void *arg) {
gpio_num_t pin = (gpio_num_t)(intptr_t)arg;
gpio_config_t io = {
.pin_bit_mask = 1ULL << pin,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io);
gpio_set_level(pin, 0);
while (g_blink_task == xTaskGetCurrentTaskHandle()) {
gpio_set_level(pin, 1);
vTaskDelay(pdMS_TO_TICKS(g_blink_delay_ms));
gpio_set_level(pin, 0);
vTaskDelay(pdMS_TO_TICKS(g_blink_delay_ms));
}
release_pin(pin);
vTaskDelete(NULL);
}
/*======== PWM ========*/
static bool setup_pwm(gpio_num_t gpio) {
if (g_pwm_mode) ledc_stop(LEDC_MODE, LEDC_CHANNEL, 0);
// Configure LEDC timer
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = g_pwm_frequency,
.clk_cfg = LEDC_AUTO_CLK
};
esp_err_t ret = ledc_timer_config(&ledc_timer);
if (ret != ESP_OK) {
printf("PWM timer config failed: %s (try lower freq or lower duty resolution)\n", esp_err_to_name(ret));
return false;
}
// Configure LEDC channel
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = gpio,
.duty = 0,
.hpoint = 0
};
ret = ledc_channel_config(&ledc_channel);
if (ret != ESP_OK) {
printf("PWM channel config failed: %s\n", esp_err_to_name(ret));
return false;
}
return true;
}
static void set_pwm_brightness(uint8_t level) {
level = (uint8_t)clamp_int(level, BRIGHT_MIN, BRIGHT_MAX);
g_pwm_brightness = level;
const uint32_t duty = brightness_to_duty(level);
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
printf("PWM brightness set to %u%% (duty: %" PRIu32 "/%" PRIu32 ")\n",
(unsigned)level, duty, ledc_max_duty());
}
static void set_pwm_frequency(uint32_t freq) {
g_pwm_frequency = clamp_u32(freq, FREQ_MIN_HZ, FREQ_MAX_HZ);
if (g_pwm_mode) {
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = g_pwm_frequency,
.clk_cfg = LEDC_AUTO_CLK
};
esp_err_t ret = ledc_timer_config(&ledc_timer);
if (ret == ESP_OK) {
printf("PWM frequency set to %" PRIu32 " Hz\n", g_pwm_frequency);
set_pwm_brightness(g_pwm_brightness); // restore duty for new timer base
} else {
printf("Failed to set PWM frequency: %s\n", esp_err_to_name(ret));
}
} else {
printf("PWM frequency set to %" PRIu32 " Hz (applies when PWM starts)\n", g_pwm_frequency);
}
}
/*======== MODE START ========*/
static void start_blink(gpio_num_t gpio) {
if (!is_test_safe(gpio)) {
printf("Restricted: GPIO %d is not in the test-safe set.\n", gpio);
return;
}
stop_all();
g_current_pin = gpio;
g_blinking = true;
if (xTaskCreatePinnedToCore(blink_task, "blink_task", BLINK_STACK, (void*)(intptr_t)gpio,
BLINK_TASK_PRIO, &g_blink_task, tskNO_AFFINITY) != pdPASS) {
printf("Error: Failed to create blink task\n");
g_blinking = false;
g_current_pin = INVALID_GPIO;
return;
}
printf("Blinking GPIO %d at %" PRIu32 " ms.\n", g_current_pin, g_blink_delay_ms);
}
static void start_pwm(gpio_num_t gpio, uint8_t brightness) {
if (!is_test_safe(gpio)) {
printf("Restricted: GPIO %d is not in the test-safe set.\n", gpio);
return;
}
stop_all();
if (!setup_pwm(gpio)) {
printf("Failed to setup PWM on GPIO %d\n", gpio);
return;
}
g_current_pin = gpio;
g_pwm_mode = true;
set_pwm_brightness(brightness);
printf("PWM started on GPIO %d at %" PRIu32 " Hz\n", gpio, g_pwm_frequency);
}
/*======== COMMAND PARSER ========*/
static void trim(char *s) {
size_t len = strlen(s);
while (len && (s[len-1] == '\r' || s[len-1] == '\n' || isspace((unsigned char)s[len-1]))) s[--len] = 0;
size_t i = 0;
while (s[i] && isspace((unsigned char)s[i])) i++;
if (i) memmove(s, s+i, strlen(s+i)+1);
}
static bool all_digits(const char *s) {
if (!*s) return false;
for (const char *p = s; *p; ++p) if (!isdigit((unsigned char)*p)) return false;
return true;
}
static void handle_command(char *line) {
trim(line);
if (!*line) return;
char cmd[64];
strncpy(cmd, line, sizeof(cmd)-1);
cmd[sizeof(cmd)-1] = 0;
for (char *p = cmd; *p; ++p) *p = (char)tolower((unsigned char)*p);
if (strcmp(cmd, "list") == 0) {
print_test_safe_pins();
return;
}
if (strcmp(cmd, "status") == 0) {
printf("Pin: %d, mode: %s, ",
(int)g_current_pin, g_blinking ? "blink" : (g_pwm_mode ? "pwm" : "idle"));
if (g_blinking) {
printf("delay: %" PRIu32 " ms\n", g_blink_delay_ms);
} else if (g_pwm_mode) {
printf("brightness: %u%%, frequency: %" PRIu32 " Hz\n",
(unsigned)g_pwm_brightness, g_pwm_frequency);
} else {
printf("no active output\n");
}
return;
}
if (strcmp(cmd, "stop") == 0 || strcmp(cmd, "pwm off") == 0 || strcmp(cmd, "mode off") == 0) {
stop_all();
return;
}
if (strncmp(cmd, "pin ", 4) == 0) {
int gpio = atoi(line + 4);
start_blink((gpio_num_t)gpio);
return;
}
if (strncmp(cmd, "delay ", 6) == 0) {
int v = atoi(line + 6);
g_blink_delay_ms = clamp_u32((uint32_t)v, BLINK_DELAY_MIN_MS, BLINK_DELAY_MAX_MS);
printf("Set blink delay to %" PRIu32 " ms.\n", g_blink_delay_ms);
return;
}
if (strncmp(cmd, "pwm ", 4) == 0) {
// "pwm <pin> [brightness]"
char *args = line + 4; trim(args);
int gpio = -1, brightness = g_pwm_brightness;
if (sscanf(args, "%d %d", &gpio, &brightness) >= 1) {
brightness = clamp_int(brightness, BRIGHT_MIN, BRIGHT_MAX);
start_pwm((gpio_num_t)gpio, (uint8_t)brightness);
} else {
printf("Usage: pwm <pin> [brightness %d-%d]\nCurrent brightness: %u%%\n",
BRIGHT_MIN, BRIGHT_MAX, (unsigned)g_pwm_brightness);
}
return;
}
if (strncmp(cmd, "brightness ", 11) == 0 || strncmp(cmd, "bright ", 7) == 0) {
const char *arg = (strncmp(cmd, "bright ", 7) == 0) ? (line + 7) : (line + 11);
int brightness = atoi(arg);
brightness = clamp_int(brightness, BRIGHT_MIN, BRIGHT_MAX);
if (g_pwm_mode) {
set_pwm_brightness((uint8_t)brightness);
} else {
printf("Error: PWM mode not active. Use 'pwm <pin> [brightness]' first.\n");
}
return;
}
if (strncmp(cmd, "freq ", 5) == 0 || strncmp(cmd, "frequency ", 10) == 0) {
char *arg = (cmd[4] == ' ') ? (line + 5) : (line + 10);
int freq = atoi(arg);
if (freq > 0) set_pwm_frequency((uint32_t)freq);
else printf("Frequency must be positive\n");
return;
}
if (strcmp(cmd, "help") == 0) {
printf("Commands:\n"
" list -> show test-safe GPIOs\n"
" status -> show current pin/mode\n"
" pin <gpio> -> start blinking GPIO\n"
" <gpio> -> same as 'pin <gpio>'\n"
" delay <ms> -> set blink delay (%u..%u)\n"
" pwm <pin> [brightness] -> start PWM (%d..%d, current: %u%%)\n"
" brightness <level> -> set PWM brightness (%d..%d)\n"
" freq <frequency> -> set PWM frequency (Hz, %u..%u)\n"
" pwm off | mode off | stop -> stop output and release pin\n",
BLINK_DELAY_MIN_MS, BLINK_DELAY_MAX_MS,
BRIGHT_MIN, BRIGHT_MAX, (unsigned)g_pwm_brightness,
BRIGHT_MIN, BRIGHT_MAX,
FREQ_MIN_HZ, FREQ_MAX_HZ);
return;
}
if (all_digits(line)) { // bare number = pin
start_blink((gpio_num_t)atoi(line));
return;
}
printf("Unknown. Type 'help' for commands.\n");
}
/*======== CONSOLE TASK ========*/
static void console_task(void *arg) {
const uart_port_t uart_num = UART_NUM_0;
char line[CLI_LINE_MAX] = {0};
size_t pos = 0; // length used
size_t cursor = 0; // caret position
char hist[HIST_MAX][CLI_LINE_MAX] = {{0}};
int hist_count = 0;
int hist_head = 0;
int hist_view = HIST_NONE;
cli_ctx_t C = {
.uart = uart_num,
.line = line,
.pos = &pos,
.cursor = &cursor,
.hist = hist,
.hist_count = &hist_count,
.hist_head = &hist_head,
.hist_view = &hist_view
};
cli_prompt(&C);
enum { ESC_IDLE, ESC_ESC, ESC_CSI } esc = ESC_IDLE;
char csi_param_buf[4] = {0};
int csi_param_len = 0;
while (1) {
uint8_t ch;
int got = uart_read_bytes(uart_num, &ch, 1, pdMS_TO_TICKS(30));
if (got != 1) continue;
// Escape handling (arrows/Home/End/Del and Alt-B/F)
if (esc == ESC_ESC) {
if (ch == '[') { esc = ESC_CSI; csi_param_len = 0; continue; }
// Alt-b / Alt-f
if (ch == 'b' || ch == 'B') {
if (cursor > 0) {
while (cursor > 0 && isspace((unsigned char)line[cursor-1])) cursor--;
while (cursor > 0 && !isspace((unsigned char)line[cursor-1])) cursor--;
cli_redraw(&C);
}
esc = ESC_IDLE; continue;
}
if (ch == 'f' || ch == 'F') {
if (cursor < pos) {
while (cursor < pos && !isspace((unsigned char)line[cursor])) cursor++;
while (cursor < pos && isspace((unsigned char)line[cursor])) cursor++;
cli_redraw(&C);
}
esc = ESC_IDLE; continue;
}
esc = ESC_IDLE; // unknown ESC seq
continue;
} else if (esc == ESC_CSI) {
if (ch >= '0' && ch <= '9') {
if (csi_param_len < (int)sizeof(csi_param_buf)-1) csi_param_buf[csi_param_len++] = (char)ch;
continue;
}
if (ch == '~') {
// [3~ delete, [1~ home, [4~ end
int p = atoi(csi_param_buf);
if (p == 3) { // Delete forward
if (cursor < pos) {
memmove(&line[cursor], &line[cursor+1], pos - cursor - 1);
pos--; line[pos] = 0;
cli_redraw(&C);
}
} else if (p == 1) {
cursor = 0; cli_redraw(&C);
} else if (p == 4) {
cursor = pos; cli_redraw(&C);
}
esc = ESC_IDLE; continue;
}
// Final byte for standard arrows/home/end
if (ch == 'A') { // Up
if (hist_count) {
if (!cli_in_hist(&C)) hist_view = (hist_head - 1 + HIST_MAX) % HIST_MAX;
else {
int oldest = (hist_head - hist_count + HIST_MAX) % HIST_MAX;
if (hist_view != oldest) hist_view = (hist_view - 1 + HIST_MAX) % HIST_MAX;
}
cli_load_hist(&C, hist_view);
}
} else if (ch == 'B') { // Down
if (cli_in_hist(&C)) {
int newest = (hist_head - 1 + HIST_MAX) % HIST_MAX;
if (hist_view != newest) { hist_view = (hist_view + 1) % HIST_MAX; cli_load_hist(&C, hist_view); }
else { hist_view = HIST_NONE; pos = cursor = 0; line[0] = 0; cli_redraw(&C); }
}
} else if (ch == 'C') { // Right
if (cursor < pos) { cursor++; cli_redraw(&C); }
} else if (ch == 'D') { // Left
if (cursor > 0) { cursor--; cli_redraw(&C); }
} else if (ch == 'H') { // Home
cursor = 0; cli_redraw(&C);
} else if (ch == 'F') { // End
cursor = pos; cli_redraw(&C);
}
esc = ESC_IDLE;
continue;
}
if (ch == 0x1B) { esc = ESC_ESC; continue; } // start ESC
// CR / LF -> execute
if (ch == '\r' || ch == '\n') {
cli_write(&C, "\r\n");
line[pos] = 0;
if (pos) { cli_push_hist(&C, line); hist_view = HIST_NONE; }
handle_command(line);
pos = cursor = 0; line[0] = 0;
cli_prompt(&C);
continue;
}
// Ctrl-A / Ctrl-E
if (ch == 0x01) { cursor = 0; cli_redraw(&C); continue; }
if (ch == 0x05) { cursor = pos; cli_redraw(&C); continue; }
// Backspace
if (ch == 0x08 || ch == 0x7F) {
if (cursor > 0) {
memmove(&line[cursor - 1], &line[cursor], pos - cursor);
cursor--; pos--;
line[pos] = 0;
cli_redraw(&C);
}
continue;
}
// Ctrl-U: clear line
if (ch == 0x15) { pos = cursor = 0; line[0] = 0; cli_redraw(&C); continue; }
// Ctrl-L: redraw
if (ch == 0x0C) { cli_redraw(&C); continue; }
// Ctrl-W: delete previous word
if (ch == 0x17) {
size_t start = cursor;
while (start > 0 && isspace((unsigned char)line[start-1])) start--;
while (start > 0 && !isspace((unsigned char)line[start-1])) start--;
if (start < cursor) {
memmove(&line[start], &line[cursor], pos - cursor);
pos -= (cursor - start);
cursor = start;
line[pos] = 0;
cli_redraw(&C);
}
continue;
}
// Printable ASCII: insert at cursor
if (isprint(ch)) {
if (pos + 1 < CLI_LINE_MAX) {
if (cli_in_hist(&C)) { hist_view = HIST_NONE; /* keep recalled text */ }
memmove(&line[cursor + 1], &line[cursor], pos - cursor);
line[cursor++] = (char)ch;
pos++;
line[pos] = 0;
cli_redraw(&C);
}
continue;
}
// ignore other control bytes
}
}
/*======== APP ========*/
void app_main(void) {
const int baud = 115200;
const uart_port_t uart_num = UART_NUM_0;
uart_config_t cfg = {
.baud_rate = baud,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
ESP_ERROR_CHECK(uart_driver_install(uart_num, UART_RX_BUF, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(uart_num, &cfg));
uart_vfs_dev_use_driver(uart_num);
uart_vfs_dev_port_set_rx_line_endings(uart_num, ESP_LINE_ENDINGS_CRLF);
uart_vfs_dev_port_set_tx_line_endings(uart_num, ESP_LINE_ENDINGS_CRLF);
vTaskDelay(pdMS_TO_TICKS(200));
uart_flush_input(uart_num);
printf("\nESP32-S3 GPIO Tester with PWM (ESP-IDF)\n");
printf("Type 'help' for commands. Cursor keys, history, word jumps supported.\n");
print_test_safe_pins();
xTaskCreatePinnedToCore(console_task, "console", CONSOLE_STACK, NULL,
CONSOLE_TASK_PRIO, NULL, tskNO_AFFINITY);
// Don't return; keep prompt active
vTaskDelete(NULL);
}