Console

About

The Console library provides an interactive command-line interface (CLI) for ESP32 applications. It wraps the ESP-IDF console component and exposes an Arduino-style API for registering commands, starting a background REPL (Read-Eval-Print Loop) task, managing command history, and parsing typed arguments using the argtable3 library.

Features include:

  • Background REPL task with configurable stack, priority, and core affinity.

  • Tab completion and inline hints powered by the linenoise library.

  • Persistent command history saved to LittleFS or SPIFFS.

  • Typed argument parsing via argtable3 (integers, strings, optional arguments, etc.).

  • Context-aware command callbacks for object-oriented design.

  • Transport-agnostic: works with UART Serial and HWCDC / USB-JTAG (HWCDCSerial) — the same transport that Serial uses is detected automatically from the ARDUINO_USB_MODE / ARDUINO_USB_CDC_ON_BOOT build flags.

Note

The Console REPL currently does not support USB OTG (CDC via TinyUSB / USBSerial). On boards where USB OTG is selected as the Serial transport, the REPL will not receive input. Use UART or HWCDC instead.

Note that some features like history and tab completion are only available on terminals that support VT100 escape sequences. The Arduino IDE Serial Monitor does not support them. If needed, you can use any other terminal that supports VT100 escape sequences, such as PuTTY, Minicom or Picocom. If your terminal prints weird sequences of characters like [5n, it means that the terminal does not support VT100 escape sequences. To disable VT100 escape sequences, you can use the setPlainMode() method and use a plain text mode instead.

The Console library is usable by all ESP32 variants. Depending on how the board’s USB and Serial are implemented, you may need to ensure that USB CDC is enabled on boot and use the Hardware CDC. For example, on the most recent ESP32-P4 revisions, you need to enable USB CDC On Boot and set the USB Mode to Hardware CDC and JTAG.

Header File

#include <Console.h>

Overview

The library exposes a single global object Console of type ConsoleClass.

Typical usage:

  1. (Optional) Call configuration methods such as setPrompt(), setMaxHistory(), and setHistoryFile() before calling begin().

  2. Call Console.begin() to initialize the underlying esp_console module.

  3. Register commands with Console.addCmd() or Console.addCmdWithContext().

  4. Call Console.addHelpCmd() to register the built-in help command.

  5. Call Console.attachToSerial(true) to start the interactive session in a background task.

Alternatively, skip attachToSerial(true) and call Console.run() manually from your own input loop (see the ConsoleManual example).

Two function-pointer typedefs are provided for command callbacks:

// Simple callback
typedef int (*ConsoleCommandFunc)(int argc, char **argv);

// Context-aware callback (receives a user-supplied pointer as the first argument)
typedef int (*ConsoleCommandFuncWithCtx)(void *context, int argc, char **argv);

Both argtable3 (<console/argtable3/argtable3.h>) and linenoise (<console/linenoise/linenoise.h>) headers are included by Console.h and are directly available in your sketch.

Argument Parsing with argtable3

The Console library re-exports the argtable3 C library, which provides GNU-style argument parsing for your commands. Instead of manually parsing argv strings, you declare typed argument descriptors and let argtable handle validation, error messages, and automatic hint generation.

Declaring Arguments

Each argument is created by calling a constructor function. The two most common variants are:

  • arg_xxx1(...)required argument (exactly 1 occurrence).

  • arg_xxx0(...)optional argument (0 or 1 occurrence).

Available types:

Constructor

Value field

Description

arg_int0 / arg_int1

->ival[0]

Integer (decimal, hex 0x, or octal 0 prefix)

arg_dbl0 / arg_dbl1

->dval[0]

Double-precision floating point

arg_str0 / arg_str1

->sval[0]

String (no whitespace unless quoted)

arg_lit0 / arg_lit1

->count

Boolean flag (no value — presence means true)

arg_end

(none)

Sentinel that terminates the table and stores parse errors

Every constructor takes four parameters:

struct arg_xxx* arg_xxx1(
    const char *shortopts,   // Short option letter, e.g. "p" for -p (or NULL)
    const char *longopts,    // Long option name, e.g. "pin" for --pin (or NULL)
    const char *datatype,    // Placeholder shown in hints, e.g. "<pin>"
    const char *glossary     // Description shown in help text
);

When both shortopts and longopts are NULL, the argument is positional (matched by order, not by a flag).

Building an Argument Table

Group all argument descriptors into a struct that ends with arg_end:

// Declare a global struct for the argument table
static struct {
  struct arg_int *pin;     // positional, required: GPIO pin number
  struct arg_int *value;   // positional, required: 0 or 1
  struct arg_end *end;     // must always be last — tracks parse errors
} my_args;

void setup() {
  // Allocate and initialize each argument descriptor.
  // arg_int1: required int arg.  (NULL, NULL) = positional (no -x/--xx flag).
  my_args.pin   = arg_int1(NULL, NULL, "<pin>", "GPIO pin number");
  my_args.value = arg_int1(NULL, NULL, "<0|1>", "0 = LOW, 1 = HIGH");
  // arg_end(N): sentinel — N is the max number of errors to store.
  my_args.end   = arg_end(2);

  // Register the command, passing &my_args as the argtable.
  // The Console library auto-generates the hint string from the table.
  Console.addCmd("gpio", "Write a GPIO pin", &my_args, cmd_gpio);
}

Parsing Inside a Command Handler

Inside the command callback, call arg_parse() to validate the arguments and fill in the values:

static int cmd_gpio(int argc, char **argv) {
  // Parse argv against the argument table
  int nerrors = arg_parse(argc, argv, (void **)&my_args);
  if (nerrors != 0) {
    // Print human-readable error messages to stdout
    arg_print_errors(stdout, my_args.end, argv[0]);
    return 1;
  }

  // Access the parsed values through the typed fields
  int pin = my_args.pin->ival[0];   // first (and only) int value
  int val = my_args.value->ival[0];

  pinMode(pin, OUTPUT);
  digitalWrite(pin, val ? HIGH : LOW);
  printf("GPIO %d = %d\n", pin, val);
  return 0;
}

Optional Arguments and Flags

Use arg_xxx0 for optional parameters, and check ->count to see if they were supplied:

static struct {
  struct arg_str *ssid;      // required positional string
  struct arg_str *password;  // optional positional string
  struct arg_lit *verbose;   // optional flag: -v / --verbose
  struct arg_end *end;
} connect_args;

void setup() {
  connect_args.ssid     = arg_str1(NULL, NULL, "<ssid>", "Network name");
  connect_args.password = arg_str0(NULL, NULL, "[<password>]", "Password (optional)");
  connect_args.verbose  = arg_lit0("v", "verbose", "Print extra info");
  connect_args.end      = arg_end(3);
  // ...
}

static int cmd_connect(int argc, char **argv) {
  int nerrors = arg_parse(argc, argv, (void **)&connect_args);
  if (nerrors != 0) {
    arg_print_errors(stdout, connect_args.end, argv[0]);
    return 1;
  }

  const char *ssid = connect_args.ssid->sval[0];

  // Check if the optional password was supplied
  const char *pass = (connect_args.password->count > 0)
                     ? connect_args.password->sval[0]
                     : "";

  // Check if the -v / --verbose flag was present
  bool verbose = (connect_args.verbose->count > 0);

  // ...
  return 0;
}

See the ConsoleGPIO and ConsoleWiFi examples for complete working sketches.

Arduino-esp32 Console API

begin

Initialize the console module. Must be called before any other method.

bool begin(size_t maxCmdLen = 256, size_t maxArgs = 32)

Parameters

  • maxCmdLen (Optional)
    • Maximum length of a single command-line in bytes. Default: 256.

  • maxArgs (Optional)
    • Maximum number of whitespace-separated tokens on a line. Default: 32.

Returns
  • true on success; false if esp_console_init() fails.

Notes
  • Sets up linenoise tab-completion and hints automatically.

  • Loads history from the file set by setHistoryFile() if one was configured.

  • Calling begin() again after it has already succeeded has no effect.

end

De-initialize the console module.

void end()
Notes
  • Stops the REPL task if running.

  • Saves command history to the configured file before freeing resources.

setPrompt

Set the prompt string displayed at the start of each input line.

void setPrompt(const char *prompt)
void setPrompt(const String &prompt)
Parameters
  • prompt — Prompt string. Default: "esp> ".

Notes
  • Must be called before begin() to take effect in the REPL task.

setMaxHistory

Set the maximum number of command history entries kept in RAM.

void setMaxHistory(uint32_t maxLen)
Parameters
  • maxLen — Maximum history entries. Default: 32.

setHistoryFile

Set the filesystem and file path for persistent command history.

void setHistoryFile(fs::FS &fs, const char *path)
void setHistoryFile(fs::FS &fs, const String &path)

The method combines fs.mountpoint() and path internally to build the full VFS path used by linenoise for loading and saving history. You only need to pass the filesystem-relative path.

Parameters
  • path — File path relative to the filesystem root, e.g. "/history.txt".

  • fs — A mounted Arduino filesystem object. Accepts LittleFS, SPIFFS, FFat, etc.

Notes
  • History is loaded from this file at begin() and saved after every command.

  • The filesystem must be mounted before begin() is called.

Example

LittleFS.begin(true);
Console.setHistoryFile(LittleFS, "/history.txt");

setTaskStackSize

Set the REPL background task stack size in bytes.

void setTaskStackSize(uint32_t size)
Parameters
  • size — Stack size in bytes. Default: 4096.

setTaskPriority

Set the REPL background task priority.

void setTaskPriority(uint32_t priority)
Parameters
  • priority — FreeRTOS task priority. Default: 2.

setTaskCore

Pin the REPL task to a specific CPU core.

void setTaskCore(BaseType_t core)
Parameters
  • core — Core index (0 or 1) or tskNO_AFFINITY. Default: tskNO_AFFINITY.

usePsram

Route all Console PSRAM-eligible allocations to external SPI RAM.

void usePsram(bool enable)

When enabled:

  • The esp_console command registry (heap_alloc_caps in esp_console_config_t) is allocated from PSRAM.

  • The REPL FreeRTOS task stack is allocated from PSRAM via xTaskCreatePinnedToCoreWithCaps().

This frees internal SRAM for other uses at the cost of slightly higher latency for stack and heap accesses. Has no effect if PSRAM is not available or not initialized at boot.

Parameters
  • enabletrue to use PSRAM, false for internal RAM (default: true).

Notes
  • Must be called before begin() to affect heap allocation, and before attachToSerial(true) to affect the task stack.

Example

Console.usePsram(true);  // before begin()
Console.begin();
Console.attachToSerial(true);

addCmd

Register a command. Three overloads are available depending on how the hint is provided.

// Auto-generated hint (no hint / hint from argtable)
bool addCmd(const char *name, const char *help, ConsoleCommandFunc func)
bool addCmd(const String &name, const String &help, ConsoleCommandFunc func)

// Explicit hint string
bool addCmd(const char *name, const char *help, const char *hint, ConsoleCommandFunc func)
bool addCmd(const String &name, const String &help, const String &hint, ConsoleCommandFunc func)

// argtable3 argument table (hint auto-generated from the table)
bool addCmd(const char *name, const char *help, void *argtable, ConsoleCommandFunc func)

Parameters

  • name (Required)
    • Command name as typed at the prompt. Must not contain spaces. The pointer must remain valid until end() is called.

  • help (Required)
    • Help text shown by the built-in help command.

  • hint (Optional)
    • Short argument synopsis shown inline while typing, e.g. "<pin> <value>".

  • argtable (Optional)
    • Pointer to an argtable3 argument table (a struct ending with arg_end). The hint is generated automatically from the table. The table is only read during registration.

  • func (Required)
    • Command handler. Receives argc / argv where argv[0] is the command name. Return 0 for success, non-zero for failure.

Returns
  • true on success.

addCmdWithContext

Register a context-aware command.

bool addCmdWithContext(const char *name, const char *help,
                       ConsoleCommandFuncWithCtx func, void *ctx)
bool addCmdWithContext(const String &name, const String &help,
                       ConsoleCommandFuncWithCtx func, void *ctx)

bool addCmdWithContext(const char *name, const char *help, const char *hint,
                       ConsoleCommandFuncWithCtx func, void *ctx)
bool addCmdWithContext(const String &name, const String &help, const String &hint,
                       ConsoleCommandFuncWithCtx func, void *ctx)

bool addCmdWithContext(const char *name, const char *help, void *argtable,
                       ConsoleCommandFuncWithCtx func, void *ctx)

Parameters

  • func — Callback of type ConsoleCommandFuncWithCtx. The context pointer is passed as the first argument on every call.

  • ctx — Arbitrary pointer forwarded to func as its first argument.

All other parameters are the same as addCmd.

Returns
  • true on success.

removeCmd

Unregister a previously registered command.

bool removeCmd(const char *name)
bool removeCmd(const String &name)
Parameters
  • name — Name of the command to remove.

Returns
  • true on success; false if the command was not found.

addHelpCmd

Register the built-in help command.

bool addHelpCmd()

When called with no arguments, help lists all registered commands with their hints and help text. When called with a command name (e.g. help gpio), it prints the details for that command only.

Returns
  • true on success.

removeHelpCmd

Remove the built-in help command.

bool removeHelpCmd()
Returns
  • true on success.

setHelpVerboseLevel

Control the verbosity of the built-in help command.

bool setHelpVerboseLevel(int level)
Parameters
  • level
    • 0 — Brief: show command name and hint only.

    • 1 — Verbose: also show the full help text.

Returns
  • true on success; false if level is out of range.

attachToSerial

Attach or detach the console from the serial port.

bool attachToSerial(bool enable)

When enable is true, starts the Read-Eval-Print Loop in a background FreeRTOS task. The task reads input through Serial (bypassing VFS stdin to avoid a race condition with the UART/USB driver ISR) and passes each line to esp_console_run(). Non-empty entries are added to the command history. Because reading goes through Serial, no transport-specific configuration is required — UART and HWCDC work automatically.

When enable is false, stops the background task and frees the serial port for other use.

Note

USB OTG (CDC via TinyUSB / USBSerial) is not currently supported as a REPL transport.

At startup, the task probes the terminal for VT100 support by sending a one-time Device Status Request (ESC[5n). If the terminal does not respond within 500 ms, plain mode is enabled automatically. Use setPlainMode() before attachToSerial(true) to override the probe result.

Parameters
  • enabletrue to attach and start the REPL task, false to detach and stop it.

Returns
  • true on success.

Notes
  • begin() must be called before attachToSerial(true).

  • Calling attachToSerial(true) a second time has no effect.

  • Calling attachToSerial(false) when not attached has no effect.

isAttachedToSerial

Check whether the console is currently attached to the serial port.

bool isAttachedToSerial()
Returns
  • true if the REPL background task is running.

run

Execute a command-line string directly without a REPL task.

int run(const char *cmdline)
int run(const String &cmdline)
Parameters
  • cmdline — Full command-line (command name followed by arguments).

Returns
  • The command handler’s return code (0 = success).

  • -1 if the command was not found or an esp_console error occurred.

Notes
  • run() can be called from inside a command handler to invoke other commands (command composition). However, the underlying IDF function shares a single parse buffer, so the caller’s argv pointers are invalidated after the call returns. Copy any argv values you need into local variables before calling run().

    static int cmd_led(int argc, char **argv) {
      // Read argv BEFORE calling run() — it becomes invalid after.
      bool turnOn = (strcmp(argv[1], "on") == 0);
    
      if (turnOn) {
        Console.run("gpio write 2 1");
      }
      // Do NOT use argv[1] here.
      return 0;
    }
    
  • History is not updated by run(). Manage history manually when building a custom input loop (see the ConsoleManual example).

clearScreen

Clear the terminal screen.

void clearScreen()

setMultiLine

Enable or disable multi-line editing mode.

void setMultiLine(bool enable)
Parameters
  • enabletrue to allow the input line to wrap across multiple terminal rows. Disabled by default.

setPlainMode

Enable or disable plain (non-VT100) input mode.

void setPlainMode(bool enable, bool force = true)

In plain mode linenoise falls back to basic line input without cursor movement, arrow-key history navigation, or tab-completion rendering. Useful for terminals that do not support ANSI/VT100 escape sequences (such as the Arduino IDE Serial Monitor).

By default, attachToSerial(true) probes the terminal at startup (sending a one-time Device Status Request) and enables plain mode automatically if the terminal does not respond.

Parameters
  • enabletrue to enable plain mode, false to restore VT100 mode.

  • force — When true (default), the automatic VT100 probe at attachToSerial(true) startup is skipped and the mode set here is kept unconditionally. When false, the mode is applied immediately but attachToSerial(true) may still override it based on the probe result.

Notes
  • This wraps linenoise’s internal “dumb mode”, a legacy term from the era of character-only display terminals. The Arduino API uses the friendlier name.

isPlainMode

Check whether plain (non-VT100) input mode is currently active.

bool isPlainMode()

Returns true if plain mode is enabled, either because it was set explicitly with setPlainMode(true) or because the automatic VT100 probe at attachToSerial(true) startup detected a non-VT100 terminal.

Returns
  • true if plain mode is active, false otherwise.

splitArgv

Split a command-line string into argv-style tokens in place.

static size_t Console.splitArgv(char *line, char **argv, size_t argv_size)

Handles quoted strings (spaces preserved, quotes stripped) and backslash escapes. Modifies the input buffer.

Parameters

  • line — Null-terminated input string, modified in place.

  • argv — Output array; each element will point into line.

  • argv_size — Size of the argv array. At most argv_size - 1 tokens are returned; argv[argc] is always set to NULL.

Returns
  • Number of tokens found (argc).

Example

char line[] = "gpio write 5 1";
char *argv[8];
size_t argc = Console.splitArgv(line, argv, 8);
// argc == 4, argv[0]=="gpio", argv[1]=="write", argv[2]=="5", argv[3]=="1"

Examples

ConsoleBasic

Full interactive REPL with history, tab completion, and inline hints.

/*
 * ConsoleBasic — Basic interactive REPL with history and tab completion.
 *
 * This example demonstrates the basic usage of the Console library.
 * It uses the REPL (Read-Eval-Print Loop) API to create an interactive shell.
 * These are the available commands:
 *
 *   version — Print firmware version
 *   heap    — Show free heap memory
 *   restart — Restart the device
 *   echo    — Echo arguments back
 *
 * Connect with any serial terminal (115200 baud) and type "help" to list
 * available commands. Note that for full functionality (like history and tab completion),
 * you need to use a serial terminal that supports VT100 escape sequences (Arduino IDE does not).
 *
 * Board support: all ESP32 variants using UART or HWCDC (USB-JTAG).
 * Currently, the Console library does not support USB OTG (CDC via TinyUSB / USBSerial).
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>

// ---------------------------------------------------------------------------
// Command: version
// ---------------------------------------------------------------------------
static int cmd_version(int argc, char **argv) {
  printf("arduino-esp32 %s\n", ESP_ARDUINO_VERSION_STR);
  return 0;
}

// ---------------------------------------------------------------------------
// Command: heap
// ---------------------------------------------------------------------------
static int cmd_heap(int argc, char **argv) {
  printf("Free heap : %lu bytes\n", (unsigned long)ESP.getFreeHeap());
  printf("Min heap  : %lu bytes\n", (unsigned long)ESP.getMinFreeHeap());
  return 0;
}

// ---------------------------------------------------------------------------
// Command: restart
// ---------------------------------------------------------------------------
static int cmd_restart(int argc, char **argv) {
  printf("Restarting in 1 second...\n");
  fflush(stdout);
  delay(1000);
  ESP.restart();
  return 0;
}

// ---------------------------------------------------------------------------
// Command: echo  <message...>
// ---------------------------------------------------------------------------
static int cmd_echo(int argc, char **argv) {
  for (int i = 1; i < argc; i++) {
    printf("%s", argv[i]);
    if (i < argc - 1) {
      printf(" ");
    }
  }
  printf("\n");
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Configure Console
  Console.setPrompt("esp32> ");
  Console.setMaxHistory(32);

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("version", "Print firmware version", cmd_version);
  Console.addCmd("heap", "Show free heap memory", cmd_heap);
  Console.addCmd("restart", "Restart the device", cmd_restart);
  Console.addCmd("echo", "Echo arguments back", "<message...>", cmd_echo);

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  if (!Console.attachToSerial(true)) {
    Serial.println("REPL start failed");
  }
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleManual

Manual command execution from a custom input loop in loop(), without a background task.

/*
 * ConsoleManual — Run commands from Serial without a background REPL task.
 *
 * Demonstrates using Console.run() inside loop() to execute commands read
 * from Serial. Useful when you need full control over the input loop or
 * want to integrate the console into an existing event-driven sketch.
 *
 * Type a command (e.g. "heap" or "help") in the Serial Monitor and press
 * Enter. No background task is created.
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>

// ---------------------------------------------------------------------------
// Command: heap
// ---------------------------------------------------------------------------
static int cmd_heap(int argc, char **argv) {
  printf("Free heap : %lu bytes\n", (unsigned long)ESP.getFreeHeap());
  return 0;
}

// ---------------------------------------------------------------------------
// Command: uptime
// ---------------------------------------------------------------------------
static int cmd_uptime(int argc, char **argv) {
  unsigned long ms = millis();
  unsigned long s = ms / 1000;
  unsigned long days = s / 86400;
  unsigned long hours = (s % 86400) / 3600;
  unsigned long mins = (s % 3600) / 60;
  unsigned long secs = s % 60;
  printf("up %lu day(s), %02lu:%02lu:%02lu  (%.3f s)\n", days, hours, mins, secs, ms / 1000.0f);
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

static String inputBuffer;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Configure Console prompt
  Console.setPrompt("");  // No prompt — we print it manually below

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("heap", "Show free heap", cmd_heap);
  Console.addCmd("uptime", "Show uptime", cmd_uptime);

  // Add built-in help command
  Console.addHelpCmd();

  Serial.println("Console ready. Type a command and press Enter.");
  Serial.print("> ");
}

// In this example we manually implement the input loop instead of using the REPL task.
// This is useful when you need full control over the input loop or want to integrate the console into an existing event-driven sketch.
void loop() {
  while (Serial.available()) {
    char c = (char)Serial.read();
    if (c == '\r') {
      continue;
    }
    if (c == '\n') {
      inputBuffer.trim();
      if (inputBuffer.length() > 0) {
        Serial.println();
        Console.run(inputBuffer);
        inputBuffer = "";
      }
      Serial.print("> ");
    } else {
      Serial.print(c);  // local echo
      inputBuffer += c;
    }
  }
}

ConsoleSysInfo

System information commands: uptime, heap, tasks, version, restart.

/*
 * ConsoleSysInfo — System information CLI commands.
 *
 * Implements a small set of diagnostic commands inspired by common Unix tools:
 *
 *   uptime  — elapsed time since boot
 *   heap    — free and minimum heap memory
 *   tasks   — list FreeRTOS tasks with state and stack watermark
 *   restart — restart the device
 *   version — firmware and IDF version strings
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>

// ---------------------------------------------------------------------------
// uptime
// ---------------------------------------------------------------------------
static int cmd_uptime(int argc, char **argv) {
  unsigned long ms = millis();
  unsigned long s = ms / 1000;
  unsigned long days = s / 86400;
  unsigned long hours = (s % 86400) / 3600;
  unsigned long mins = (s % 3600) / 60;
  unsigned long secs = s % 60;
  printf("up %lu day(s), %02lu:%02lu:%02lu  (%.3f s)\n", days, hours, mins, secs, ms / 1000.0f);
  return 0;
}

// ---------------------------------------------------------------------------
// heap
// ---------------------------------------------------------------------------
static int cmd_heap(int argc, char **argv) {
  printf("Free heap    : %7lu bytes\n", (unsigned long)ESP.getFreeHeap());
  printf("Min free heap: %7lu bytes\n", (unsigned long)ESP.getMinFreeHeap());
  printf("Max alloc    : %7lu bytes\n", (unsigned long)ESP.getMaxAllocHeap());
  return 0;
}

// ---------------------------------------------------------------------------
// tasks
// ---------------------------------------------------------------------------
static int cmd_tasks(int argc, char **argv) {
  uint32_t nTasks = uxTaskGetNumberOfTasks();
  TaskStatus_t *pxTaskStatusArray = (TaskStatus_t *)malloc(nTasks * sizeof(TaskStatus_t));
  if (!pxTaskStatusArray) {
    printf("Out of memory\n");
    return 1;
  }

  uint32_t ulTotalRunTime = 0;
  nTasks = uxTaskGetSystemState(pxTaskStatusArray, nTasks, &ulTotalRunTime);

  printf("%-20s %8s %5s %5s %4s\n", "Name", "State", "Prio", "Stack", "Core");
  printf("%-20s %8s %5s %5s %4s\n", "--------------------", "--------", "-----", "-----", "----");

  static const char *stateNames[] = {"Running", "Ready", "Blocked", "Suspend", "Deleted", "Invalid"};

  for (uint32_t i = 0; i < nTasks; i++) {
    TaskStatus_t *t = &pxTaskStatusArray[i];
    int state = (int)t->eCurrentState;
    if (state > 5) {
      state = 5;
    }
    int core = (int)t->xCoreID;
    char coreStr[4];
    if (core == tskNO_AFFINITY) {
      snprintf(coreStr, sizeof(coreStr), "any");
    } else {
      snprintf(coreStr, sizeof(coreStr), "%" PRIu8, (uint8_t)core);
    }
    printf("%-20s %8s %5lu %5lu %4s\n", t->pcTaskName, stateNames[state], (unsigned long)t->uxCurrentPriority, (unsigned long)t->usStackHighWaterMark, coreStr);
  }

  free(pxTaskStatusArray);
  return 0;
}

// ---------------------------------------------------------------------------
// version
// ---------------------------------------------------------------------------
static int cmd_version(int argc, char **argv) {
  printf("Arduino-ESP32 : %s\n", ESP_ARDUINO_VERSION_STR);
  printf("ESP-IDF       : %s\n", esp_get_idf_version());
  printf("Chip          : %s rev %d\n", ESP.getChipModel(), ESP.getChipRevision());
  printf("CPU freq      : %lu MHz\n", (unsigned long)ESP.getCpuFreqMHz());
  printf("Flash size    : %lu bytes\n", (unsigned long)ESP.getFlashChipSize());
  return 0;
}

// ---------------------------------------------------------------------------
// restart
// ---------------------------------------------------------------------------
static int cmd_restart(int argc, char **argv) {
  printf("Restarting in 1 second...\n");
  fflush(stdout);
  delay(1000);
  ESP.restart();
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Configure Console prompt
  Console.setPrompt("sysinfo> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("uptime", "Show time since boot", cmd_uptime);
  Console.addCmd("heap", "Show heap memory statistics", cmd_heap);
  Console.addCmd("tasks", "List FreeRTOS tasks", cmd_tasks);
  Console.addCmd("version", "Print firmware/chip version", cmd_version);
  Console.addCmd("restart", "Restart the device", cmd_restart);

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  Console.attachToSerial(true);
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleFS

File system CLI over LittleFS: ls, cat, rm, mkdir, write, df.

/*
 * ConsoleFS — File system CLI commands over LittleFS.
 *
 * Implements a minimal shell for browsing and managing the LittleFS partition:
 *
 *   ls   [path]           — list directory contents
 *   cat  <file>           — print file contents
 *   rm   <file>           — delete a file
 *   mkdir <dir>           — create a directory
 *   write <file> <text>   — write (overwrite) text to a file
 *   df                    — show filesystem usage
 *
 * Requires LittleFS to be mounted (LittleFS.begin() called in setup).
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>
#include <LittleFS.h>

// Ensure path starts with '/' so LittleFS.open() accepts it.
// Returns a pointer to either the original string or a static buffer.
static const char *normalizePath(const char *path) {
  if (path[0] == '/') {
    return path;
  }
  static char buf[256];
  snprintf(buf, sizeof(buf), "/%s", path);
  return buf;
}

// ---------------------------------------------------------------------------
// ls [path]
// ---------------------------------------------------------------------------
static int cmd_ls(int argc, char **argv) {
  const char *path = (argc > 1) ? normalizePath(argv[1]) : "/";

  File dir = LittleFS.open(path);
  if (!dir || !dir.isDirectory()) {
    printf("ls: %s: not a directory\n", path);
    return 1;
  }

  File entry = dir.openNextFile();
  while (entry) {
    if (entry.isDirectory()) {
      printf("d  %s/\n", entry.name());
    } else {
      printf("f  %-40s  %zu bytes\n", entry.name(), entry.size());
    }
    entry = dir.openNextFile();
  }
  return 0;
}

// ---------------------------------------------------------------------------
// cat <file>
// ---------------------------------------------------------------------------
static int cmd_cat(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: cat <file>\n");
    return 1;
  }

  const char *path = normalizePath(argv[1]);
  File f = LittleFS.open(path, "r");
  if (!f) {
    printf("cat: %s: no such file\n", path);
    return 1;
  }

  while (f.available()) {
    int c = f.read();
    putchar(c);
  }
  f.close();
  printf("\n");
  return 0;
}

// ---------------------------------------------------------------------------
// rm <file>
// ---------------------------------------------------------------------------
static int cmd_rm(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: rm <file>\n");
    return 1;
  }

  const char *path = normalizePath(argv[1]);
  if (!LittleFS.exists(path)) {
    printf("rm: %s: no such file\n", path);
    return 1;
  }

  if (!LittleFS.remove(path)) {
    printf("rm: %s: failed\n", path);
    return 1;
  }

  printf("removed '%s'\n", path);
  return 0;
}

// ---------------------------------------------------------------------------
// mkdir <dir>
// ---------------------------------------------------------------------------
static int cmd_mkdir(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: mkdir <dir>\n");
    return 1;
  }

  const char *path = normalizePath(argv[1]);
  if (!LittleFS.mkdir(path)) {
    printf("mkdir: %s: failed\n", path);
    return 1;
  }

  printf("created '%s'\n", path);
  return 0;
}

// ---------------------------------------------------------------------------
// write <file> <text...>
// ---------------------------------------------------------------------------
static int cmd_write(int argc, char **argv) {
  if (argc < 3) {
    printf("Usage: write <file> <text...>\n");
    return 1;
  }

  const char *path = normalizePath(argv[1]);
  File f = LittleFS.open(path, "w");
  if (!f) {
    printf("write: %s: cannot open\n", path);
    return 1;
  }

  for (int i = 2; i < argc; i++) {
    f.print(argv[i]);
    if (i < argc - 1) {
      f.print(' ');
    }
  }
  f.println();
  f.close();
  printf("written to '%s'\n", path);
  return 0;
}

// ---------------------------------------------------------------------------
// df
// ---------------------------------------------------------------------------
static int cmd_df(int argc, char **argv) {
  size_t total = LittleFS.totalBytes();
  size_t used = LittleFS.usedBytes();
  size_t free_ = total - used;
  printf("Filesystem  Size     Used     Free     Use%%\n");
  printf("LittleFS    %-8zu %-8zu %-8zu %3zu%%\n", total, used, free_, total ? (used * 100 / total) : 0);
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Mount LittleFS (format on first use)
  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS mount failed");
    return;
  }
  Serial.println("LittleFS mounted");

  // Configure Console prompt
  Console.setPrompt("fs> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("ls", "List directory contents", "[path]", cmd_ls);
  Console.addCmd("cat", "Print file contents", "<file>", cmd_cat);
  Console.addCmd("rm", "Delete a file", "<file>", cmd_rm);
  Console.addCmd("mkdir", "Create a directory", "<dir>", cmd_mkdir);
  Console.addCmd("write", "Write text to a file", "<file> <text...>", cmd_write);
  Console.addCmd("df", "Show filesystem usage", cmd_df);

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  Console.attachToSerial(true);
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleGPIO

GPIO control with argtable3 argument parsing: gpio read, gpio write, gpio mode. LED control is supported with the led command on boards with a built-in LED.

/*
 * ConsoleGPIO — GPIO control via CLI using argtable3 argument parsing.
 *
 * Implements three sub-commands accessed as a single "gpio" command:
 *
 *   gpio read  <pin>              — read and print pin level
 *   gpio write <pin> <0|1>        — write HIGH or LOW to a pin
 *   gpio mode  <pin> <mode>       — set pin mode (in/out/in_pu/in_pd)
 *   led <on|off> [color]          — toggle LED (with RGB color on supported boards)
 *
 * Demonstrates argtable3 usage: typed arguments with automatic hint
 * generation and built-in error reporting. The "led" command shows how to
 * reuse existing command handlers from a higher-level convenience command.
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>
#include "argtable3/argtable3.h"

#define RGB_BRIGHTNESS 64  // Change brightness (max 255)

// ---------------------------------------------------------------------------
// gpio read <pin>
// ---------------------------------------------------------------------------

static struct {
  struct arg_int *pin;  // required int: GPIO pin number
  struct arg_end *end;  // sentinel: tracks parse errors
} gpio_read_args;

static int cmd_gpio_read(int argc, char **argv) {
  // arg_parse() validates argv against the argument table, returns error count
  int nerrors = arg_parse(argc, argv, (void **)&gpio_read_args);
  if (nerrors != 0) {
    // Print human-readable error messages (e.g. "missing <pin> argument")
    arg_print_errors(stderr, gpio_read_args.end, argv[0]);
    return 1;
  }

  // Access the parsed integer value via ->ival[0]
  int pin = gpio_read_args.pin->ival[0];
  int val = digitalRead(pin);
  Serial.printf("GPIO %d = %d (%s)\n", pin, val, val ? "HIGH" : "LOW");
  return 0;
}

// ---------------------------------------------------------------------------
// gpio write <pin> <value>
// ---------------------------------------------------------------------------

static struct {
  struct arg_int *pin;    // required int: GPIO pin number
  struct arg_int *value;  // required int: 0 or 1
  struct arg_end *end;    // sentinel: tracks parse errors
} gpio_write_args;

static int cmd_gpio_write(int argc, char **argv) {
  int nerrors = arg_parse(argc, argv, (void **)&gpio_write_args);
  if (nerrors != 0) {
    arg_print_errors(stderr, gpio_write_args.end, argv[0]);
    return 1;
  }

  // Access the parsed integer values via ->ival[0]
  int pin = gpio_write_args.pin->ival[0];
  int val = gpio_write_args.value->ival[0];

  if (val != 0 && val != 1) {
    Serial.println("gpio write: value must be 0 or 1");
    return 1;
  }

  digitalWrite(pin, val);
  Serial.printf("GPIO %d set to %d (%s)\n", pin, val, val ? "HIGH" : "LOW");
  return 0;
}

// ---------------------------------------------------------------------------
// gpio mode <pin> <mode>
// ---------------------------------------------------------------------------

static struct {
  struct arg_int *pin;   // required int: GPIO pin number
  struct arg_str *mode;  // required string: mode name (in/out/in_pu/in_pd)
  struct arg_end *end;   // sentinel: tracks parse errors
} gpio_mode_args;

static int cmd_gpio_mode(int argc, char **argv) {
  int nerrors = arg_parse(argc, argv, (void **)&gpio_mode_args);
  if (nerrors != 0) {
    arg_print_errors(stderr, gpio_mode_args.end, argv[0]);
    return 1;
  }

  // Access the parsed integer value via ->ival[0]
  int pin = gpio_mode_args.pin->ival[0];

  // Access the parsed string value via ->sval[0]
  const char *modeStr = gpio_mode_args.mode->sval[0];

  uint8_t mode;
  if (strcmp(modeStr, "in") == 0) {
    mode = INPUT;
  } else if (strcmp(modeStr, "out") == 0) {
    mode = OUTPUT;
  } else if (strcmp(modeStr, "in_pu") == 0) {
    mode = INPUT_PULLUP;
  } else if (strcmp(modeStr, "in_pd") == 0) {
    mode = INPUT_PULLDOWN;
  } else {
    Serial.printf("gpio mode: unknown mode '%s'. Use: in, out, in_pu, in_pd\n", modeStr);
    return 1;
  }

  pinMode(pin, mode);
  Serial.printf("GPIO %d mode set to %s\n", pin, modeStr);
  return 0;
}

// ---------------------------------------------------------------------------
// Top-level dispatcher: gpio <subcmd> ...
// ---------------------------------------------------------------------------

static int cmd_gpio(int argc, char **argv) {
  if (argc < 2) {
    Serial.println("Usage: gpio <read|write|mode> ...");
    Serial.println("  gpio read  <pin>");
    Serial.println("  gpio write <pin> <0|1>");
    Serial.println("  gpio mode  <pin> <in|out|in_pu|in_pd>");
    return 1;
  }

  // Shift argv so the sub-command is argv[0]
  if (strcmp(argv[1], "read") == 0) {
    return cmd_gpio_read(argc - 1, argv + 1);
  } else if (strcmp(argv[1], "write") == 0) {
    return cmd_gpio_write(argc - 1, argv + 1);
  } else if (strcmp(argv[1], "mode") == 0) {
    return cmd_gpio_mode(argc - 1, argv + 1);
  } else {
    Serial.printf("gpio: unknown sub-command '%s'\n", argv[1]);
    return 1;
  }
}

// ---------------------------------------------------------------------------
// led <on|off> [color] — convenience command that reuses the gpio handlers
//
// When RGB_BUILTIN is defined (boards with an addressable RGB LED), an
// optional color name is accepted: red, green, blue, white, yellow.
//
// This command will reuse the gpio commands if the board does not have an RGB LED.
// Otherwise, it will use the rgbLedWrite function to set the color of the LED.
// ---------------------------------------------------------------------------

#ifdef LED_BUILTIN

#ifdef RGB_BUILTIN
struct NamedColor {
  const char *name;
  uint8_t r, g, b;
};

static const NamedColor colors[] = {
  {"red", RGB_BRIGHTNESS, 0, 0},
  {"green", 0, RGB_BRIGHTNESS, 0},
  {"blue", 0, 0, RGB_BRIGHTNESS},
  {"white", RGB_BRIGHTNESS, RGB_BRIGHTNESS, RGB_BRIGHTNESS},
  {"yellow", RGB_BRIGHTNESS, RGB_BRIGHTNESS, 0},
};

static const size_t numColors = sizeof(colors) / sizeof(colors[0]);
#else
static bool ledInitialized = false;
#endif

static void printLedUsage() {
#ifdef RGB_BUILTIN
  Serial.println("Usage: led <on [color]|off>");
  Serial.println("  Colors: red, green, blue, white, yellow");
#else
  Serial.println("Usage: led <on|off>");
#endif
}

static int cmd_led(int argc, char **argv) {
  if (argc < 2) {
    printLedUsage();
    return 1;
  }

#ifdef RGB_BUILTIN
  // Read argv values BEFORE calling Console.run() — nested run() calls
  // invalidate the caller's argv pointers (shared IDF parse buffer).
  if (strcmp(argv[1], "on") == 0) {
    // If the board has an RGB LED, and a color is specified, set the LED to the specified color.
    // If no color is specified, set the LED to white.
    // This will use the rgbLedWrite function to set the color of the LED.
    const char *colorName = (argc > 2) ? argv[2] : "white";
    for (size_t i = 0; i < numColors; i++) {
      if (strcasecmp(colorName, colors[i].name) == 0) {
        rgbLedWrite(RGB_BUILTIN, colors[i].r, colors[i].g, colors[i].b);
        Serial.printf("RGB LED on: %s (%d, %d, %d)\n", colors[i].name, colors[i].r, colors[i].g, colors[i].b);
        return 0;
      }
    }
    Serial.printf("Unknown color '%s'. Available: ", colorName);
    for (size_t i = 0; i < numColors; i++) {
      Serial.printf("%s%s", colors[i].name, (i < numColors - 1) ? ", " : "\n");
    }
    return 1;
  } else if (strcmp(argv[1], "off") == 0) {
    rgbLedWrite(RGB_BUILTIN, 0, 0, 0);
    Serial.println("RGB LED off");
    return 0;
  }
#else
  // Read argv values BEFORE calling Console.run() — nested run() calls
  // invalidate the caller's argv pointers (shared IDF parse buffer).
  if (strcmp(argv[1], "on") == 0) {
    // If the board does not have an RGB LED, use the gpio commands to toggle the LED.
    // This will call the gpio commands to configure the pin as an output and set the LED to HIGH.
    if (!ledInitialized) {
      Console.run(String("gpio mode ") + LED_BUILTIN + " out");
      ledInitialized = true;
    }
    return Console.run(String("gpio write ") + LED_BUILTIN + " 1");
  } else if (strcmp(argv[1], "off") == 0) {
    if (!ledInitialized) {
      Console.run(String("gpio mode ") + LED_BUILTIN + " out");
      ledInitialized = true;
    }
    return Console.run(String("gpio write ") + LED_BUILTIN + " 0");
  }
#endif

  printLedUsage();
  return 1;
}

#endif

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Initialize argtable argument structures for each GPIO sub-command.
  // These structures help parse and validate command line arguments for the Console commands.
  //
  // arg_int1(shortopts, longopts, datatype, glossary)
  //   - Adds a required integer argument.
  // arg_str1(...)
  //   - Adds a required string argument.
  // arg_end(N)
  //   - Terminates the argument table and provides space for up to N error messages.
  //
  // Passing NULL for both shortopts and longopts makes arguments positional (not using flags like -x or --foo).
  // The glossary string is a description that appears in the help message.

  // Arguments for: gpio read <pin>
  gpio_read_args.pin = arg_int1(NULL, NULL, "<pin>", "GPIO pin number to read (required)");
  gpio_read_args.end = arg_end(1);  // Up to 1 error message for reading.

  // Arguments for: gpio write <pin> <0|1>
  gpio_write_args.pin = arg_int1(NULL, NULL, "<pin>", "GPIO pin number to write (required)");
  gpio_write_args.value = arg_int1(NULL, NULL, "<0|1>", "Value to write: 0 = LOW, 1 = HIGH (required)");
  gpio_write_args.end = arg_end(2);  // Up to 2 error messages for writing.

  // Arguments for: gpio mode <pin> <in|out|in_pu|in_pd>
  gpio_mode_args.pin = arg_int1(NULL, NULL, "<pin>", "GPIO pin number to configure (required)");
  gpio_mode_args.mode = arg_str1(NULL, NULL, "<in|out|in_pu|in_pd>", "Pin mode: in, out, in_pu, or in_pd (required)");
  gpio_mode_args.end = arg_end(2);  // Up to 2 error messages for mode.

  // Configure Console prompt
  Console.setPrompt("gpio> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("gpio", "Control GPIO pins", "<read|write|mode> ...", cmd_gpio);

#ifdef LED_BUILTIN
#ifdef RGB_BUILTIN
  Console.addCmd("led", "Control RGB LED", "<on [color]|off>", cmd_led);
#else
  Console.addCmd("led", "Toggle LED_BUILTIN", "<on|off>", cmd_led);
#endif
#endif

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  Console.attachToSerial(true);
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleAdvanced

Demonstrates advanced APIs: context-aware commands (addCmdWithContext), dynamic command registration and removal (removeCmd), REPL pause/resume (attachToSerial(false/true)), help verbosity (setHelpVerboseLevel), clearScreen, setPlainMode, setMultiLine, and task tuning (setTaskStackSize, setTaskPriority, setTaskCore).

/*
 * ConsoleAdvanced — Demonstrates advanced Console API features.
 *
 * Covers APIs not shown in the basic examples:
 *
 *   - addCmdWithContext()        — pass state to a command via a context pointer
 *   - removeCmd()                — unregister a command at runtime
 *   - attachToSerial(false/true) — pause and resume the REPL
 *   - setHelpVerboseLevel()      — toggle brief / verbose help output
 *   - clearScreen()              — clear the terminal
 *   - setPlainMode()             — force plain (non-VT100) input
 *   - setMultiLine()             — enable multi-line editing
 *   - Task tuning: setTaskStackSize(), setTaskPriority(), setTaskCore()
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>

// ---------------------------------------------------------------------------
// Context-aware command: addCmdWithContext() demo
//
// A context pointer lets multiple commands share or isolate state without
// global variables.  The pointer is passed as the first argument to the
// callback every time the command is invoked.
// ---------------------------------------------------------------------------

struct LedCtx {
  int pin;
  bool state;
  const char *name;
};

static LedCtx led1 = {2, false, "LED1"};
static LedCtx led2 = {15, false, "LED2"};

static int cmd_toggle(void *context, int argc, char **argv) {
  LedCtx *led = (LedCtx *)context;
  led->state = !led->state;
  pinMode(led->pin, OUTPUT);
  digitalWrite(led->pin, led->state ? HIGH : LOW);
  printf("%s (GPIO %d) = %s\n", led->name, led->pin, led->state ? "ON" : "OFF");
  return 0;
}

// ---------------------------------------------------------------------------
// removeCmd demo: register/unregister a "secret" command at runtime
// ---------------------------------------------------------------------------

static int cmd_secret(int argc, char **argv) {
  printf("You found the secret command!\n");
  return 0;
}

static bool secretRegistered = false;

static int cmd_lock(int argc, char **argv) {
  if (secretRegistered) {
    Console.removeCmd("secret");
    secretRegistered = false;
    printf("'secret' command removed.\n");
  } else {
    printf("'secret' is not registered.\n");
  }
  return 0;
}

static int cmd_unlock(int argc, char **argv) {
  if (!secretRegistered) {
    Console.addCmd("secret", "A hidden command", cmd_secret);
    secretRegistered = true;
    printf("'secret' command registered. Try it!\n");
  } else {
    printf("'secret' is already available.\n");
  }
  return 0;
}

// ---------------------------------------------------------------------------
// attachToSerial(false/true) demo: pause the Read-Eval-Print Loop for 5 seconds
// ---------------------------------------------------------------------------

static int cmd_pause(int argc, char **argv) {
  printf("Stopping Read-Eval-Print Loop for 5 seconds...\n");
  fflush(stdout);
  Console.attachToSerial(false);
  delay(5000);
  Console.attachToSerial(true);
  return 0;
}

// ---------------------------------------------------------------------------
// setHelpVerboseLevel demo
// ---------------------------------------------------------------------------

static int cmd_verbose_help(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: verbose_help <0|1>\n");
    printf("  0 = brief (names and hints only)\n");
    printf("  1 = verbose (includes help text)\n");
    return 1;
  }
  int level = atoi(argv[1]);
  if (Console.setHelpVerboseLevel(level)) {
    printf("Help verbose level set to %d.\n", level);
  } else {
    printf("Invalid level. Use 0 or 1.\n");
  }
  return 0;
}

// ---------------------------------------------------------------------------
// clearScreen demo
// ---------------------------------------------------------------------------

static int cmd_clear(int argc, char **argv) {
  Console.clearScreen();
  return 0;
}

// ---------------------------------------------------------------------------
// setPlainMode / setMultiLine toggles
// ---------------------------------------------------------------------------

static int cmd_plain(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: plain <on|off>\n");
    return 1;
  }
  bool enable = (strcmp(argv[1], "on") == 0);
  Console.setPlainMode(enable);
  printf("Plain mode %s.\n", enable ? "enabled" : "disabled");
  return 0;
}

static int cmd_multiline(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: multiline <on|off>\n");
    return 1;
  }
  bool enable = (strcmp(argv[1], "on") == 0);
  Console.setMultiLine(enable);
  printf("Multi-line mode %s.\n", enable ? "enabled" : "disabled");
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Console task configuration — must be called before attachToSerial(true)

  // Set the task stack size to 4096 bytes
  Console.setTaskStackSize(4096);
  // Set the task priority to 2
  Console.setTaskPriority(2);
  // Set the task core to 1 (core 1)
  Console.setTaskCore(1);  // pin REPL to core 1

  // Configure Console prompt
  Console.setPrompt("adv> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Context-aware commands: same callback, different context pointer.
  // Each LED struct carries its own pin number, state, and label.
  Console.addCmdWithContext("led1", "Toggle LED1", cmd_toggle, &led1);
  Console.addCmdWithContext("led2", "Toggle LED2", cmd_toggle, &led2);

  // Dynamic command management
  Console.addCmd("unlock", "Register the 'secret' command", cmd_unlock);
  Console.addCmd("lock", "Unregister the 'secret' command", cmd_lock);

  // REPL control
  Console.addCmd("pause", "Stop the Read-Eval-Print Loop for 5 seconds", cmd_pause);

  // Help verbosity
  Console.addCmd("verbose_help", "Set help verbosity", "<0|1>", cmd_verbose_help);

  // Terminal utilities
  Console.addCmd("clear", "Clear the terminal screen", cmd_clear);
  Console.addCmd("plain", "Toggle plain (non-VT100) mode", "<on|off>", cmd_plain);
  Console.addCmd("multiline", "Toggle multi-line editing", "<on|off>", cmd_multiline);

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  if (!Console.attachToSerial(true)) {
    Serial.println("Failed to attach to serial");
  }
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleHistory

Persistent command history across reboots using LittleFS (setHistoryFile), PSRAM allocation (usePsram), and the splitArgv tokenising utility.

/*
 * ConsoleHistory — Persistent history, PSRAM allocation, and splitArgv demo.
 *
 * Demonstrates APIs not covered by the basic examples:
 *
 *   - setHistoryFile()  — save/load history to a filesystem across reboots
 *   - setMaxHistory()   — limit number of stored history lines
 *   - usePsram()        — allocate Console heap and REPL task stack in PSRAM
 *   - splitArgv()       — split a raw string into argv-style tokens
 *
 * On boot the REPL loads the previous session's history.  Use the up/down
 * arrow keys (VT100 terminals) or the "history" command to inspect entries.
 * Power-cycle the board to verify history persists.
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>
#include <LittleFS.h>

static const char *HISTORY_PATH = "/console_history.txt";

// ---------------------------------------------------------------------------
// Command: history — print the current history buffer
// ---------------------------------------------------------------------------
static int cmd_history(int argc, char **argv) {
  File f = LittleFS.open(HISTORY_PATH, "r");
  if (!f) {
    printf("No history saved yet.\n");
    return 0;
  }

  int index = 1;
  while (f.available()) {
    String line = f.readStringUntil('\n');
    line.trim();
    if (line.length() > 0) {
      printf("  %3d  %s\n", index++, line.c_str());
    }
  }
  f.close();

  if (index == 1) {
    printf("History is empty.\n");
  }
  return 0;
}

// ---------------------------------------------------------------------------
// Command: tokenize — demonstrate Console.splitArgv()
//
// splitArgv() splits a raw string into argc/argv tokens, handling quotes
// and backslash escapes.  Useful when you receive a full command line from
// a non-REPL source (MQTT, BLE, HTTP, etc.) and need to parse it.
// ---------------------------------------------------------------------------
static int cmd_tokenize(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: tokenize <string>\n");
    printf("  Splits the string into tokens.  Use quotes for multi-word tokens.\n");
    printf("  Example: tokenize hello \"big world\" foo\n");
    return 1;
  }

  // Reconstruct the argument string from argv[1..] so the user can type
  // something like: tokenize hello "big world" foo
  String raw;
  for (int i = 1; i < argc; i++) {
    if (i > 1) {
      raw += ' ';
    }
    raw += argv[i];
  }

  // splitArgv() works in-place — it modifies the buffer and sets pointers
  // into it.  We need a mutable copy.
  char *buf = strdup(raw.c_str());
  if (!buf) {
    printf("Out of memory\n");
    return 1;
  }

  char *tokens[16];
  size_t count = Console.splitArgv(buf, tokens, 16);

  printf("Input : \"%s\"\n", raw.c_str());
  printf("Tokens: %zu\n", count);
  for (size_t i = 0; i < count; i++) {
    printf("  [%zu] \"%s\"\n", i, tokens[i]);
  }

  free(buf);
  return 0;
}

// ---------------------------------------------------------------------------
// Command: heap — show free memory
// ---------------------------------------------------------------------------
static int cmd_heap(int argc, char **argv) {
  printf("Free heap : %lu bytes\n", (unsigned long)ESP.getFreeHeap());
  if (psramFound()) {
    printf("Free PSRAM: %lu bytes\n", (unsigned long)ESP.getFreePsram());
  }
  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Mount LittleFS (format on first use)
  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS mount failed — history will not persist.");
  } else {
    Serial.println("LittleFS mounted");
    // Persistent history: lines are stored in a plain text file on LittleFS.
    // On begin(), existing history is loaded; after every command, the file
    // is updated automatically.
    Console.setHistoryFile(LittleFS, HISTORY_PATH);
  }

  Console.setMaxHistory(50);

  // If PSRAM is available, move Console allocations there to save internal
  // SRAM. Falls back to internal RAM if PSRAM is not available.
  Console.usePsram(true);

  // Configure Console prompt
  Console.setPrompt("hist> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("history", "Show history info", cmd_history);
  Console.addCmd("tokenize", "Split a string into tokens", "<string>", cmd_tokenize);
  Console.addCmd("heap", "Show free memory", cmd_heap);

  // Add built-in help command
  Console.addHelpCmd();

  if (!Console.attachToSerial(true)) {
    Serial.println("REPL start failed");
  }
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}

ConsoleWiFi

Wi-Fi management: wifi scan, wifi connect, wifi disconnect, wifi status, wifi ping.

/*
 * ConsoleWiFi — Wi-Fi management CLI commands.
 *
 * Implements a set of commands for scanning, connecting, and monitoring Wi-Fi:
 *
 *   wifi scan                     — scan and list nearby access points
 *   wifi connect <ssid> [<pass>]  — connect to a network
 *   wifi disconnect               — disconnect from the current network
 *   wifi status                   — show connection status and IP info
 *   ping <host> [-c <count>]      — ICMP ping a host by name or IP
 *
 * Demonstrates argtable3 with optional arguments (password is optional)
 * and optional flags (-c for ping count).
 *
 * Created by lucasssvaz
 */

#include <Arduino.h>
#include <Console.h>
#include <WiFi.h>
#include "ping/ping_sock.h"
#include "argtable3/argtable3.h"

// ---------------------------------------------------------------------------
// wifi scan
// ---------------------------------------------------------------------------

static int cmd_wifi_scan(int argc, char **argv) {
  printf("Scanning...\n");
  int n = WiFi.scanNetworks();
  if (n == 0) {
    printf("No networks found.\n");
    return 0;
  }

  printf("%-4s  %-32s  %5s  %4s  %s\n", "#", "SSID", "RSSI", "Ch", "Encryption");
  printf("%-4s  %-32s  %5s  %4s  %s\n", "---", "--------------------------------", "-----", "----", "----------");

  for (int i = 0; i < n; i++) {
    const char *enc;
    switch (WiFi.encryptionType(i)) {
      case WIFI_AUTH_OPEN:         enc = "Open"; break;
      case WIFI_AUTH_WEP:          enc = "WEP"; break;
      case WIFI_AUTH_WPA_PSK:      enc = "WPA"; break;
      case WIFI_AUTH_WPA2_PSK:     enc = "WPA2"; break;
      case WIFI_AUTH_WPA_WPA2_PSK: enc = "WPA/WPA2"; break;
      case WIFI_AUTH_WPA3_PSK:     enc = "WPA3"; break;
      default:                     enc = "Unknown"; break;
    }
    printf("%-4d  %-32s  %5d  %4d  %s\n", i + 1, WiFi.SSID(i).c_str(), (int)WiFi.RSSI(i), (int)WiFi.channel(i), enc);
  }

  WiFi.scanDelete();
  return 0;
}

// ---------------------------------------------------------------------------
// wifi connect <ssid> [<password>]
// ---------------------------------------------------------------------------

static struct {
  struct arg_str *ssid;      // required positional string: network name
  struct arg_str *password;  // optional positional string: network password
  struct arg_end *end;       // sentinel: tracks parse errors
} wifi_connect_args;

static int cmd_wifi_connect(int argc, char **argv) {
  int nerrors = arg_parse(argc, argv, (void **)&wifi_connect_args);
  if (nerrors != 0) {
    arg_print_errors(stdout, wifi_connect_args.end, argv[0]);
    return 1;
  }

  const char *ssid = wifi_connect_args.ssid->sval[0];  // access parsed string via ->sval[0]
  // For optional args, check ->count to see if it was provided
  const char *pass = (wifi_connect_args.password->count > 0) ? wifi_connect_args.password->sval[0] : NULL;

  printf("Connecting to '%s'...\n", ssid);
  WiFi.STA.connect(ssid, pass);

  uint32_t deadline = millis() + 15000;
  while (!WiFi.STA.connected() && millis() < deadline) {
    delay(500);
    printf(".");
    fflush(stdout);
  }
  printf("\n");

  if (WiFi.STA.connected()) {
    printf("Connected. IP: %s\n", WiFi.STA.localIP().toString().c_str());
    return 0;
  } else {
    printf("Connection failed (status %d)\n", (int)WiFi.STA.status());
    return 1;
  }
}

// ---------------------------------------------------------------------------
// wifi disconnect
// ---------------------------------------------------------------------------

static int cmd_wifi_disconnect(int argc, char **argv) {
  WiFi.STA.disconnect();
  printf("Disconnected.\n");
  return 0;
}

// ---------------------------------------------------------------------------
// wifi status
// ---------------------------------------------------------------------------

static int cmd_wifi_status(int argc, char **argv) {
  wl_status_t s = WiFi.STA.status();
  const char *statusStr;
  switch (s) {
    case WL_CONNECTED:      statusStr = "Connected"; break;
    case WL_NO_SSID_AVAIL:  statusStr = "SSID not found"; break;
    case WL_CONNECT_FAILED: statusStr = "Connect failed"; break;
    case WL_DISCONNECTED:   statusStr = "Disconnected"; break;
    case WL_IDLE_STATUS:    statusStr = "Idle"; break;
    default:                statusStr = "Unknown"; break;
  }

  printf("Status    : %s\n", statusStr);

  if (s == WL_CONNECTED) {
    printf("SSID      : %s\n", WiFi.STA.SSID().c_str());
    printf("BSSID     : %s\n", WiFi.STA.BSSIDstr().c_str());
    printf("RSSI      : %d dBm\n", (int)WiFi.STA.RSSI());
    printf("IP        : %s\n", WiFi.STA.localIP().toString().c_str());
    printf("Gateway   : %s\n", WiFi.STA.gatewayIP().toString().c_str());
    printf("DNS       : %s\n", WiFi.STA.dnsIP().toString().c_str());
    printf("Subnet    : %s\n", WiFi.STA.subnetMask().toString().c_str());
  }

  printf("MAC       : %s\n", WiFi.STA.macAddress().c_str());
  return 0;
}

// ---------------------------------------------------------------------------
// Top-level dispatcher: wifi <subcmd> ...
// ---------------------------------------------------------------------------

static int cmd_wifi(int argc, char **argv) {
  if (argc < 2) {
    printf("Usage: wifi <scan|connect|disconnect|status>\n");
    return 1;
  }

  if (strcmp(argv[1], "scan") == 0) {
    return cmd_wifi_scan(argc - 1, argv + 1);
  } else if (strcmp(argv[1], "connect") == 0) {
    return cmd_wifi_connect(argc - 1, argv + 1);
  } else if (strcmp(argv[1], "disconnect") == 0) {
    return cmd_wifi_disconnect(argc - 1, argv + 1);
  } else if (strcmp(argv[1], "status") == 0) {
    return cmd_wifi_status(argc - 1, argv + 1);
  } else {
    printf("wifi: unknown sub-command '%s'\n", argv[1]);
    return 1;
  }
}

// ---------------------------------------------------------------------------
// ping <host> [-c <count>]
// ---------------------------------------------------------------------------

static struct {
  struct arg_str *host;   // required positional: hostname or IP address
  struct arg_int *count;  // optional: -c <count> (default 5)
  struct arg_end *end;
} ping_args;

static SemaphoreHandle_t ping_done_sem = NULL;

static void ping_on_success(esp_ping_handle_t hdl, void *args) {
  uint32_t elapsed;
  uint32_t size;
  uint32_t seqno;
  uint8_t ttl;
  ip_addr_t addr;
  esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
  esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
  esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
  esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed, sizeof(elapsed));
  esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
  printf(
    "%lu bytes from %s: icmp_seq=%lu ttl=%d time=%lu ms\n", (unsigned long)size, ipaddr_ntoa(&addr), (unsigned long)seqno, (int)ttl, (unsigned long)elapsed
  );
}

static void ping_on_timeout(esp_ping_handle_t hdl, void *args) {
  uint32_t seqno;
  ip_addr_t addr;
  esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
  esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
  printf("From %s icmp_seq=%lu timeout\n", ipaddr_ntoa(&addr), (unsigned long)seqno);
}

static void ping_on_end(esp_ping_handle_t hdl, void *args) {
  uint32_t transmitted;
  uint32_t received;
  uint32_t total_ms;
  esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
  esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
  esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_ms, sizeof(total_ms));
  uint32_t loss = transmitted ? ((transmitted - received) * 100 / transmitted) : 0;
  printf("\n--- ping statistics ---\n");
  printf(
    "%lu packets transmitted, %lu received, %lu%% packet loss, time %lu ms\n", (unsigned long)transmitted, (unsigned long)received, (unsigned long)loss,
    (unsigned long)total_ms
  );
  xSemaphoreGive(ping_done_sem);
}

static int cmd_ping(int argc, char **argv) {
  int nerrors = arg_parse(argc, argv, (void **)&ping_args);
  if (nerrors != 0) {
    arg_print_errors(stdout, ping_args.end, argv[0]);
    return 1;
  }

  if (!WiFi.STA.connected()) {
    printf("Not connected to Wi-Fi.\n");
    return 1;
  }

  const char *host = ping_args.host->sval[0];
  int count = (ping_args.count->count > 0) ? ping_args.count->ival[0] : 5;
  if (count < 1) {
    count = 1;
  }

  IPAddress addr;
  if (!Network.hostByName(host, addr)) {
    printf("ping: unknown host '%s'\n", host);
    return 1;
  }

  ip_addr_t target_addr;
  target_addr.type = IPADDR_TYPE_V4;
  target_addr.u_addr.ip4.addr = (uint32_t)addr;

  printf("PING %s (%s): 64 bytes of data, count=%d\n", host, addr.toString().c_str(), count);
  fflush(stdout);

  esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
  ping_config.target_addr = target_addr;
  ping_config.count = count;

  esp_ping_callbacks_t cbs = {};
  cbs.on_ping_success = ping_on_success;
  cbs.on_ping_timeout = ping_on_timeout;
  cbs.on_ping_end = ping_on_end;

  esp_ping_handle_t ping_handle = NULL;
  if (esp_ping_new_session(&ping_config, &cbs, &ping_handle) != ESP_OK) {
    printf("Failed to create ping session\n");
    return 1;
  }

  esp_ping_start(ping_handle);
  xSemaphoreTake(ping_done_sem, portMAX_DELAY);
  esp_ping_delete_session(ping_handle);

  return 0;
}

// ---------------------------------------------------------------------------
// setup / loop
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  // Initialize WiFi
  WiFi.STA.begin();

  // Create semaphore for ping done
  ping_done_sem = xSemaphoreCreateBinary();

  // Initialize argtable.
  // arg_str1() = required string.  arg_str0() = optional string.
  // NULL, NULL for shortopts/longopts makes the argument positional.
  wifi_connect_args.ssid = arg_str1(NULL, NULL, "<ssid>", "Network SSID");
  wifi_connect_args.password = arg_str0(NULL, NULL, "[<password>]", "Network password (optional for open networks)");
  wifi_connect_args.end = arg_end(2);  // sentinel — stores up to 2 parse errors

  // arg_int0() = optional integer.  "c" is the short option (-c), "count"
  // is the long option (--count).  Default ping count is 5 if not given.
  ping_args.host = arg_str1(NULL, NULL, "<host>", "Hostname or IP address to ping");
  ping_args.count = arg_int0("c", "count", "<n>", "Number of pings (default 5)");
  ping_args.end = arg_end(2);

  // Configure Console prompt
  Console.setPrompt("wifi> ");

  // Initialize Console
  if (!Console.begin()) {
    Serial.println("Console init failed");
    return;
  }

  // Add commands
  Console.addCmd("wifi", "Manage Wi-Fi connections", "<scan|connect|disconnect|status>", cmd_wifi);
  Console.addCmd("ping", "ICMP ping a host", (void *)&ping_args, cmd_ping);

  // Add built-in help command
  Console.addHelpCmd();

  // Begin Read-Eval-Print Loop
  Console.attachToSerial(true);
}

void loop() {
  // Loop task is not used in this example, so we delete it to free up resources
  vTaskDelete(NULL);
}