Examples

Annotated examples showing xelp on different platforms and use cases. Full source code is in the examples/ directory.

ExamplePlatformWhat it demonstrates
Bare metalAny MCUMinimal porting template with all three modes
Multi-instanceAny MCUTwo independent CLIs on separate UARTs
Posix simpleLinux / macOSFull interactive demo with ncurses
ArduinoAny Arduino boardLED control, token listing via CLI
Arduino C++Any Arduino boardC++ wrapper class for easy integration
ESP32 WiFiESP32WiFi config, time and weather fetch via CLI
ScriptingLinux / macOSBatch scripting vs interactive mode
Pico CLI (C)Raspberry Pi PicoGPIO, ADC, PWM control over USB serial
Pico CLI (Arduino Easy API)Raspberry Pi PicoTier 3 Easy API with lambdas

Bare metal

File: examples/bare-metal/bare-metal-example.c

The starting point for any new port. Shows the complete xelp integration pattern with no dependencies beyond a UART.

#include "xelp.h"

/* 1. Platform stubs -- replace with your hardware */
static void uart_putc(char c)   { /* UART TX */ }
static void uart_bksp(void)     { uart_putc('\b'); uart_putc(' '); uart_putc('\b'); }
static int  uart_rx_ready(void) { return 0; }
static char uart_getc(void)     { return 0; }

/* 2. KEY mode commands */
XELPKeyFuncMapEntry key_commands[] = {
    { &key_help,   '?', "show help"    },
    { &key_banner, 'b', "print banner" },
    XELP_FUNC_ENTRY_LAST
};

/* 3. CLI mode commands */
XELPCLIFuncMapEntry cli_commands[] = {
    { &cmd_help, "help", "show help"        },
    { &cmd_echo, "echo", "echo args back"   },
    { &cmd_info, "info", "show system info" },
    XELP_FUNC_ENTRY_LAST
};

/* 4. Wire everything up */
XELP cli;

void main(void) {
    XelpInit(&cli, "My Device v1.0\n");

    XELP_SET_FN_OUT(cli,  &uart_putc);
    XELP_SET_FN_BKSP(cli, &uart_bksp);
    XELP_SET_FN_KEY(cli,  key_commands);
    XELP_SET_FN_CLI(cli,  cli_commands);

    XelpHelp(&cli);
    XelpParseKey(&cli, '\n');   /* show initial prompt */

    for (;;) {
        if (uart_rx_ready())
            XelpParseKey(&cli, uart_getc());
    }
}

Key points

Multi-instance

File: examples/multi-instance/multi-instance-example.c

Two completely independent CLIs on separate serial ports. Each has its own output function, command table, prompt, and internal state.

XELP debug_cli, field_cli;

XelpInit(&debug_cli, "Debug Console (UART0)");
XelpInit(&field_cli, "Service Port (UART1)");

XELP_SET_FN_OUT(debug_cli, &uart0_putc);
XELP_SET_FN_OUT(field_cli, &uart1_putc);

XELP_SET_FN_CLI(debug_cli, debug_commands);
XELP_SET_FN_CLI(field_cli, field_commands);

XELP_SET_VAL_CLI_PROMPT(debug_cli, "dbg>");
XELP_SET_VAL_CLI_PROMPT(field_cli, "svc>");

/* Poll both UARTs */
for (;;) {
    if (uart0_rx_ready()) XelpParseKey(&debug_cli, uart0_getc());
    if (uart1_rx_ready()) XelpParseKey(&field_cli, uart1_getc());
}

Use cases

Posix simple

File: examples/posix-simple/xelp-example.c

Full interactive demo for Linux and macOS using ncurses for terminal handling.

cd examples/posix-simple
make

Requires ncurses: sudo apt-get install libncurses5-dev (Debian/Ubuntu).

Features demonstrated

Architecture

stdin (ncurses getch)
  |
  v
XelpParseKey(&example, char)
  |
  +--> CLI: line buffer -> tokenize -> dispatch
  +--> KEY: immediate dispatch
  +--> THR: pass-through function
  |
  v
gPutChar() -> ncurses addch() -> terminal

Arduino

File: examples/arduino/arduino.ino

Basic example using the raw C API. Works on any Arduino board with a Serial port — LED control, token listing, and built-in help. No external library dependencies.

void writeChar(char c) { Serial.write(c); }

void setup() {
    Serial.begin(115200);
    XelpInit(&cli, "xelp Arduino example v1.0\n");
    XELP_SET_FN_OUT(cli, &writeChar);
    XELP_SET_FN_CLI(cli, gMyCLICommands);
}

void loop() {
    if (Serial.available() > 0) {
        char c = Serial.read();
        XelpParseKey(&cli, c);
    }
}

Commands

Arduino C++

File: examples/arduino-cpp/arduino-cpp.ino

Same idea as the arduino example but uses the XelpCLI C++ wrapper class from src/XelpArduino.h. Eliminates boilerplate: no manual XELP_SET_FN_* macros, no Serial.available() loop — just begin(), setCommands(), and poll(Serial).

ESP32 WiFi

File: examples/esp32-wifi/esp32-wifi.ino

ESP32 WiFi example using the C++ wrapper. Configure WiFi credentials over the serial CLI, then fetch the current time and weather from free APIs (worldtimeapi.org and open-meteo.com). No API keys needed.

Commands

Scripting

File: examples/scripting/scripting-example.c

Demonstrates the difference between scripting mode (XelpParse / XelpParseXB — execute a buffer of commands at once) and interactive mode (XelpParseKey — character-by-character terminal input).

cd examples/scripting
make

Pico CLI (C)

Files: examples/pico-cli/

Pure C example for Raspberry Pi Pico, Pico W, Pico 2, and Pico 2 W using the Pico SDK. Controls GPIO, ADC, and PWM over USB CDC serial. Automatically detects Pico W boards and uses the CYW43 driver for the wireless-chip LED.

mkdir build && cd build
cmake -DPICO_BOARD=pico ..   # or pico_w, pico2, pico2_w
make

Commands

Key snippet

XELP cli;

int main(void) {
    stdio_init_all();
    gpio_init(PICO_DEFAULT_LED_PIN);
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);

    XelpInit(&cli, "Pico CLI (xelp)\n");
    XELP_SET_FN_OUT(cli, &uart_putc_fn);
    XELP_SET_FN_CLI(cli, cli_commands);

    for (;;) {
        int c = getchar_timeout_us(0);
        if (c >= 0) XelpParseKey(&cli, (char)c);
    }
}

Pico CLI (Arduino Easy API)

Files: examples/pico-cli-arduino/

Showcases the C++ Easy API on Raspberry Pi Pico using the Arduino-Pico core. Commands are registered with commands({...}) — no static tables, no raw XELP* pointers. Lambda callbacks receive a XelpCLI& reference, argc, and argv — similar to a standard main().

Key snippet

XelpCLI cli;

void setup() {
    Serial.begin(115200);
    cli.begin("Pico CLI Demo\n", nullptr);
    cli.output(Serial);
    cli.prompt("pico>");

    cli.commands({
        {"help", "show help", [](XelpCLI& c, int, const char**) {
            c.help();
        }},
        {"led", "led <0|1>", [](XelpCLI& c, int argc, const char** argv) {
            if (argc < 2) { c.print("usage: led <0|1>\n"); return; }
            int v = argv[1][0] - '0';
            digitalWrite(LED_BUILTIN, v ? HIGH : LOW);
            c.print(v ? "LED ON\n" : "LED OFF\n");
        }},
    });

    cli.keyCommands({
        {'l', "toggle LED", [](XelpCLI& c, XELPKEYCODE) {
            static bool on = false;
            on = !on;
            digitalWrite(LED_BUILTIN, on ? HIGH : LOW);
        }},
    });
}

void loop() { cli.poll(Serial); }

Two ways to register commands

Writing your own commands

All CLI commands have the same signature:

XELPRESULT my_command(XELP *ths, const char *args, int maxlen);

Pattern: parsing arguments

XELPRESULT cmd_set(XELP *ths, const char *args, int len) {
    XelpBuf b, tok;
    int n;

    XELP_XB_INIT(b, args, len);
    XelpNumToks(&b, &n);

    if (n < 3) {
        XelpOut(ths, "usage: set <key> <value>\n", 0);
        return XELP_E_ERR;
    }

    /* Token 0 = "set", Token 1 = key, Token 2 = value */
    XELP_XB_TOP(b);
    XelpTokN(&b, 1, &tok);
    /* tok.s .. tok.p is the key string */

    XELP_XB_TOP(b);
    XelpTokN(&b, 2, &tok);
    int value = XelpStr2Int(tok.s, tok.p - tok.s);

    return XELP_S_OK;
}

Pattern: output without printf

xelp has no printf. Use XelpOut for strings. For numbers, use your platform’s sprintf into a local buffer, or write a simple helper:

void print_int(XELP *x, int val) {
    char buf[12];
    int i = 0;
    if (val < 0) { XelpOut(x, "-", 1); val = -val; }
    if (val == 0) { XelpOut(x, "0", 1); return; }
    while (val > 0) { buf[i++] = '0' + (val % 10); val /= 10; }
    while (i > 0) { XelpOut(x, &buf[--i], 1); }
}

Next steps