From e868886c627cbc188f4d303dcf941353d8ad1664 Mon Sep 17 00:00:00 2001 From: deftio Date: Fri, 1 May 2026 17:23:24 -0700 Subject: [PATCH 1/3] updated ci and examples. no xelp core changes --- .github/workflows/ci.yml | 9 +- .github/workflows/release.yml | 43 ++- dev/arduino-example-todo.md | 81 +++++ examples/arduino-live-cli/README.md | 78 +++++ .../arduino-live-cli/arduino-live-cli.ino | 304 ++++++++++++++++++ examples/posix-simple-cpp/Makefile | 17 +- examples/posix-simple/Makefile | 7 +- examples/scripting/Makefile | 7 +- makefile | 48 ++- tools/make_release.sh | 62 +++- 10 files changed, 617 insertions(+), 39 deletions(-) create mode 100644 dev/arduino-example-todo.md create mode 100644 examples/arduino-live-cli/README.md create mode 100644 examples/arduino-live-cli/arduino-live-cli.ino diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0685c16..67b79a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,11 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libncurses-dev + sudo apt-get install -y libncurses-dev cppcheck + + - name: Install dependencies (macOS) + if: matrix.os == 'macos-latest' + run: brew install cppcheck - name: Set compiler run: | @@ -35,6 +39,9 @@ jobs: echo "CC=clang" >> $GITHUB_ENV fi + - name: Lint (cppcheck static analysis) + run: make lint + - name: Validate (tests + examples, -Werror) run: make validate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81e463f..d323dbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libncurses-dev + sudo apt-get install -y libncurses-dev cppcheck - name: Extract version from xelp.h id: version @@ -40,6 +40,9 @@ jobs: exit 1 fi + - name: Lint (cppcheck static analysis) + run: make lint + - name: Validate (tests + examples, -Werror) run: make validate @@ -83,3 +86,41 @@ jobs: body_path: /tmp/release_notes.md draft: false prerelease: false + + pio-publish: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: pip install platformio + + - name: Publish to PlatformIO registry + run: pio pkg publish . --no-interactive + env: + PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }} + + idf-publish: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install idf-component-manager + run: pip install idf-component-manager + + - name: Pack and upload to ESP-IDF Component Registry + run: | + compote component pack --name xelp + compote component upload --name xelp + env: + IDF_COMPONENT_API_TOKEN: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/dev/arduino-example-todo.md b/dev/arduino-example-todo.md new file mode 100644 index 0000000..25e3dc2 --- /dev/null +++ b/dev/arduino-example-todo.md @@ -0,0 +1,81 @@ +# Arduino Mega CLI Example — TODO + +Interactive serial CLI demo for Arduino Mega (and compatible boards). +Registers hardware commands via `XelpArduino.h` so users can drive pins, +read sensors, and play tones from any serial terminal. + +Target audience: Arduino beginners/intermediate users looking for a +ready-made serial command interface. Doubles as article material for +Arduino forums. + +## Commands + +| Command | Args | Description | +|---------|------|-------------| +| `setpin` | ` <0\|1>` | `digitalWrite` — set a digital pin HIGH or LOW | +| `getpin` | `` | `digitalRead` — read a digital pin, print 0 or 1 | +| `pinmode` | ` ` | `pinMode` — configure pin direction | +| `setpwm` | ` <0-255>` | `analogWrite` — set PWM duty cycle | +| `readadc` | `` | `analogRead` — read analog pin, print 0-1023 | +| `tone` | ` [dur_ms]` | `tone()` — play a frequency on a buzzer/speaker | +| `notone` | `` | `noTone()` — stop tone output | +| `pulsein` | ` [timeout_us]` | `pulseIn()` — measure pulse width in microseconds | +| `delay` | `` | `delay()` — pause for N milliseconds (useful in scripts) | +| `millis` | — | Print uptime in milliseconds | +| `micros` | — | Print uptime in microseconds | +| `scanpins` | `[first] [last]` | Read all digital + analog pins in range, print table | +| `info` | — | Print board type, SRAM free, uptime, xelp version | +| `help` | — | List available commands (built-in xelp) | +| `reboot` | — | reboots, prints the startup banner | +| 'setprompt' | `string-new prompt` | if easy do this, set the prompt by saving a new global null terminated str (max 32 chars) | + +## File Layout + +``` +examples/ + arduino-live-cli/ + arduino-live-cli.ino # main sketch + README.md # wiring notes, screenshot, usage +``` + +## Implementation Notes + +- Use `XelpArduino.h` C++ wrapper (already in repo) +- Each command is a plain C callback registered with `XelpAddCommand()` +- Argument parsing via `strtol()` / `atoi()` — keep it simple, no extra deps +- Default serial baud: 115200 +- Should compile on Mega 2560, Uno, Nano, Leonardo — nothing Mega-specific + in the API, but Mega has the most pins to play with +- Guard dangerous pins (0, 1 = Serial TX/RX) with a warning, don't block + +## Script Demo + +Show that xelp can run multi-command scripts over serial: + +``` +pinmode 13 out; setpin 13 1; delay 500; setpin 13 0; delay 500; setpin 13 1 +``` + +Blink the onboard LED from a one-liner — good for the article. + +## Arduino env + +- Add board entry to `platformio.ini` (e.g. `megaatmega2560`) +- CI already builds `.ino` examples — just needs the new directory + +## BLE Variant (Future) + +- Same command set, but over BLE serial (Nano 33 BLE / ESP32) +- Lets you drive pins wirelessly from a phone or laptop terminal +- Separate example directory: `examples/arduino-ble-cli/` +- Depends on ArduinoBLE or ESP32 BLE libraries — different scope + +## Article Outline + +1. What is xelp — one-paragraph intro +2. Wiring photo / Fritzing (LED + buzzer + potentiometer on Mega) +3. Flash the sketch, open Serial Monitor +4. Walk through commands: read a pot, blink an LED, play a tone +5. Show scripting: one-liner blink sequence +6. Link to repo + Arduino Library Manager install +7. Mention BLE variant as teaser diff --git a/examples/arduino-live-cli/README.md b/examples/arduino-live-cli/README.md new file mode 100644 index 0000000..4dc83c0 --- /dev/null +++ b/examples/arduino-live-cli/README.md @@ -0,0 +1,78 @@ +# arduino-live-cli + +Interactive hardware CLI for Arduino boards. Drive pins, read sensors, +and play tones from any serial terminal. + +## Requirements + +- Any Arduino board with a Serial port (Uno, Nano, Mega 2560, Leonardo, etc.) +- Arduino IDE 1.8+ or Arduino Cloud + +## Quick start + +1. Open `arduino-live-cli.ino` in the Arduino IDE +2. Select your board and port +3. Upload +4. Open Serial Monitor at **115200 baud** +5. Type `help` (or just `?`) + +## Commands + +| Command | Usage | Description | +|---------|-------|-------------| +| `help` | `help` | Show all commands | +| `?` | `?` | Same as help | +| `banner` | `banner` | Print xelp ASCII art and welcome message | +| `echo` | `echo ` | Echo arguments back | +| `info` | `info` | Board type, free SRAM, uptime, xelp version | +| `setpin` | `setpin <0\|1>` | `digitalWrite` -- set HIGH or LOW | +| `getpin` | `getpin ` | `digitalRead` -- print 0 or 1 | +| `pinmode` | `pinmode ` | Configure pin direction | +| `setpwm` | `setpwm <0-255>` | `analogWrite` -- set PWM duty cycle | +| `readadc` | `readadc ` | `analogRead` -- print 0-1023 | +| `tone` | `tone [ms]` | Play a frequency on a buzzer/speaker | +| `notone` | `notone ` | Stop tone output | +| `pulsein` | `pulsein [timeout]` | Measure pulse width (microseconds) | +| `delay` | `delay ` | Pause for N milliseconds | +| `millis` | `millis` | Print uptime in milliseconds | +| `micros` | `micros` | Print uptime in microseconds | +| `scanpins` | `scanpins [first] [last]` | Read all digital pins in range | +| `setprompt` | `setprompt ` | Change the CLI prompt string | +| `demo-blink3` | `demo-blink3` | Blink LED 3x -- scripting demo | +| `demo-scan` | `demo-scan` | Configure + scan pins -- scripting demo | +| `demo-info` | `demo-info` | Chain info commands -- scripting demo | +| `reboot` | `reboot` | Software reset (AVR / ESP32) | + +## Command history + +Use the up/down arrow keys to recall the last 4 commands. + +## Scripting + +xelp supports semicolon-separated command scripts. Blink the onboard +LED from a one-liner: + +``` +pinmode 13 out; setpin 13 1; delay 500; setpin 13 0; delay 500; setpin 13 1 +``` + +The `demo-*` commands show this in action -- type `demo-blink3` to see +the script printed and then executed. + +## Wiring + +No external wiring required -- the built-in LED on pin 13 works out of +the box. For the full demo, connect: + +- **LED** on any digital pin (with resistor) +- **Buzzer** on a PWM-capable pin (for `tone`) +- **Potentiometer** on an analog pin (for `readadc`) + +## Notes + +- Pins 0 and 1 are Serial RX/TX. Commands that modify them print a + warning but do not block the operation. +- `reboot` uses a watchdog reset on AVR and `ESP.restart()` on ESP32. + On other platforms it prints an error. +- Free SRAM reporting is AVR-only; other platforms show xelp version + and uptime only. diff --git a/examples/arduino-live-cli/arduino-live-cli.ino b/examples/arduino-live-cli/arduino-live-cli.ino new file mode 100644 index 0000000..66381c8 --- /dev/null +++ b/examples/arduino-live-cli/arduino-live-cli.ino @@ -0,0 +1,304 @@ +/* + * arduino-live-cli.ino -- Interactive hardware CLI for Arduino boards. + * + * Drive pins, read sensors, and play tones from any serial terminal. + * Works on Uno, Nano, Mega 2560, Leonardo, and compatible boards. + * + * Open Serial Monitor at 115200 baud and type "help". + * + * Script demo -- blink the onboard LED from a one-liner: + * pinmode 13 out; setpin 13 1; delay 500; setpin 13 0; delay 500; setpin 13 1 + * + * Command history: use up/down arrow keys to recall previous commands. + * + * Copyright (C) 2011-2026 M. A. Chatterjee + * BSD-2-Clause -- see LICENSE.txt + */ + +#define XELP_MAX_CLI_CMDS 24 +#include "xelp.h" +#include "XelpArduino.h" + +/* ------------------------------------------------------------------ */ +/* Globals */ +/* ------------------------------------------------------------------ */ + +XelpCLI cli; + +static char promptBuf[32] = "xelp>"; + +void myOutput(char c) { Serial.write(c); } + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/* Warn (but don't block) if pin is Serial RX/TX. */ +static void warnSerial(int pin) { + if (pin == 0 || pin == 1) + Serial.println(F(" WARNING: pin 0/1 is Serial RX/TX")); +} + +/* Free SRAM (AVR only). */ +#if defined(__AVR__) +static int freeMemory() { + extern int __heap_start, *__brkval; + int v; + return (int)&v - (__brkval == 0 + ? (int)&__heap_start : (int)__brkval); +} +#else +static int freeMemory() { return -1; } +#endif + +/* ------------------------------------------------------------------ */ +/* Demo scripts */ +/* ------------------------------------------------------------------ */ + +static const char DEMO_BLINK[] = + "pinmode 13 out; setpin 13 1; delay 300; setpin 13 0; delay 300; " + "setpin 13 1; delay 300; setpin 13 0; delay 300; " + "setpin 13 1; delay 300; setpin 13 0"; + +static const char DEMO_SCAN[] = + "pinmode 2 in; pinmode 3 in; pinmode 4 in; scanpins 2 4"; + +/* ------------------------------------------------------------------ */ +/* Setup */ +/* ------------------------------------------------------------------ */ + +void setup() { + Serial.begin(115200); + while (!Serial) ; /* wait for USB serial (Leonardo) */ + pinMode(LED_BUILTIN, OUTPUT); + + cli.begin("arduino-live-cli -- xelp hardware CLI demo\n", &myOutput); + cli.setPrompt(promptBuf); + + cli.commands({ + + /* ---- Help & info ------------------------------------------ */ + {"help", "show this help listing", + [](XelpCLI& c, int, const char**) { + c.help(); + }}, + + {"?", "same as help", + [](XelpCLI& c, int, const char**) { + c.help(); + }}, + + {"banner", "print xelp banner", + [](XelpCLI& c, int, const char**) { + c.print(XELP_BANNER_STR); + c.print("Welcome to xelp CLI demo.\n"); + c.print("Type help to see commands. (also accepts ?)\n"); + }}, + + {"echo", "echo -- print arguments", + [](XelpCLI& c, int argc, const char** argv) { + for (int i = 1; i < argc; i++) { + if (i > 1) c.print(" "); + c.print(argv[i]); + } + c.print("\n"); + }}, + + {"info", "print board type, memory, uptime", + [](XelpCLI& c, int, const char**) { + char buf[64]; +#if defined(ARDUINO_BOARD) + snprintf(buf, sizeof(buf), "Board: %s\n", ARDUINO_BOARD); +#else + snprintf(buf, sizeof(buf), "Board: unknown\n"); +#endif + c.print(buf); + int mem = freeMemory(); + if (mem >= 0) { + snprintf(buf, sizeof(buf), "Free: %d bytes\n", mem); + c.print(buf); + } + snprintf(buf, sizeof(buf), "Uptime: %lu ms\n", millis()); + c.print(buf); + snprintf(buf, sizeof(buf), "xelp: 0x%08lX\n", + (unsigned long)XELP_VERSION); + c.print(buf); + }}, + + /* ---- Digital I/O ------------------------------------------ */ + {"setpin", "setpin <0|1> -- digitalWrite", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 3) { c.print("usage: setpin <0|1>\n"); return; } + int pin = atoi(argv[1]); + warnSerial(pin); + digitalWrite(pin, atoi(argv[2]) ? HIGH : LOW); + }}, + + {"getpin", "getpin -- digitalRead", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 2) { c.print("usage: getpin \n"); return; } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", digitalRead(atoi(argv[1]))); + c.print(buf); + }}, + + {"pinmode", "pinmode ", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 3) { c.print("usage: pinmode \n"); return; } + int pin = atoi(argv[1]); + warnSerial(pin); + if (strcmp(argv[2], "out") == 0) pinMode(pin, OUTPUT); + else if (strcmp(argv[2], "in") == 0) pinMode(pin, INPUT); + else if (strcmp(argv[2], "pullup") == 0) pinMode(pin, INPUT_PULLUP); + else c.print(" expected: in, out, or pullup\n"); + }}, + + /* ---- Analog ----------------------------------------------- */ + {"setpwm", "setpwm <0-255> -- analogWrite", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 3) { c.print("usage: setpwm <0-255>\n"); return; } + int pin = atoi(argv[1]); + warnSerial(pin); + analogWrite(pin, atoi(argv[2])); + }}, + + {"readadc", "readadc -- analogRead (0-1023)", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 2) { c.print("usage: readadc \n"); return; } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", analogRead(atoi(argv[1]))); + c.print(buf); + }}, + + /* ---- Tone ------------------------------------------------- */ + {"tone", "tone [ms] -- play frequency", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 3) { c.print("usage: tone [ms]\n"); return; } + int pin = atoi(argv[1]); + unsigned int freq = (unsigned int)atoi(argv[2]); + if (argc >= 4) + tone(pin, freq, (unsigned long)atol(argv[3])); + else + tone(pin, freq); + }}, + + {"notone", "notone -- stop tone", + [](XelpCLI&, int argc, const char** argv) { + if (argc < 2) return; + noTone(atoi(argv[1])); + }}, + + /* ---- Pulse measurement ------------------------------------ */ + {"pulsein", "pulsein [timeout_us]", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 3) { c.print("usage: pulsein [timeout_us]\n"); return; } + int pin = atoi(argv[1]); + int level = (strcmp(argv[2], "high") == 0) ? HIGH : LOW; + unsigned long timeout = (argc >= 4) + ? (unsigned long)atol(argv[3]) : 1000000UL; + unsigned long dur = pulseIn(pin, level, timeout); + char buf[24]; + snprintf(buf, sizeof(buf), "%lu us\n", dur); + c.print(buf); + }}, + + /* ---- Timing ----------------------------------------------- */ + {"delay", "delay -- pause execution", + [](XelpCLI&, int argc, const char** argv) { + if (argc >= 2) delay((unsigned long)atol(argv[1])); + }}, + + {"millis", "print uptime in milliseconds", + [](XelpCLI& c, int, const char**) { + char buf[16]; + snprintf(buf, sizeof(buf), "%lu\n", millis()); + c.print(buf); + }}, + + {"micros", "print uptime in microseconds", + [](XelpCLI& c, int, const char**) { + char buf[16]; + snprintf(buf, sizeof(buf), "%lu\n", micros()); + c.print(buf); + }}, + + /* ---- Pin scan --------------------------------------------- */ + {"scanpins", "scanpins [first] [last] -- read digital pins", + [](XelpCLI& c, int argc, const char** argv) { + int first = (argc >= 2) ? atoi(argv[1]) : 0; + int last = (argc >= 3) ? atoi(argv[2]) : NUM_DIGITAL_PINS - 1; + char buf[24]; + c.print("pin value\n"); + c.print("--- -----\n"); + for (int p = first; p <= last; p++) { + snprintf(buf, sizeof(buf), "%3d %5d\n", p, digitalRead(p)); + c.print(buf); + } + }}, + + /* ---- Prompt ----------------------------------------------- */ + {"setprompt", "setprompt -- change CLI prompt", + [](XelpCLI& c, int argc, const char** argv) { + if (argc < 2) { c.print("usage: setprompt \n"); return; } + strncpy(promptBuf, argv[1], sizeof(promptBuf) - 1); + promptBuf[sizeof(promptBuf) - 1] = '\0'; + c.setPrompt(promptBuf); + }}, + + /* ---- Demos ------------------------------------------------ */ + {"demo-blink3", "blink LED 3x (scripting demo)", + [](XelpCLI& c, int, const char**) { + c.print("xelp can chain commands as scripts using semicolons.\n"); + c.print("Running:\n "); + c.print(DEMO_BLINK); + c.print("\n\n"); + c.run(DEMO_BLINK); + c.print("Done.\n"); + }}, + + {"demo-scan", "configure + scan pins (scripting demo)", + [](XelpCLI& c, int, const char**) { + c.print("Set pins 2-4 as inputs, then scan them:\n "); + c.print(DEMO_SCAN); + c.print("\n\n"); + c.run(DEMO_SCAN); + }}, + + {"demo-info", "echo + info + millis (scripting demo)", + [](XelpCLI& c, int, const char**) { + static const char script[] = + "echo --- board status ---; info; echo uptime:; millis"; + c.print("Chain multiple info commands in one line:\n "); + c.print(script); + c.print("\n\n"); + c.run(script); + }}, + + /* ---- Reboot ----------------------------------------------- */ + {"reboot", "software reset", + [](XelpCLI& c, int, const char**) { + c.print("Rebooting...\n"); + delay(100); +#if defined(__AVR__) + void (*resetFunc)(void) = 0; + resetFunc(); +#elif defined(ESP32) || defined(ESP8266) + ESP.restart(); +#else + c.print(" not supported on this board\n"); +#endif + }}, + }); + + Serial.println(F(XELP_BANNER_STR)); + Serial.println(F("Welcome to xelp CLI demo.")); + Serial.println(F("Type help to see commands. (also accepts ?)\n")); +} + +/* ------------------------------------------------------------------ */ +/* Loop */ +/* ------------------------------------------------------------------ */ + +void loop() { + cli.poll(Serial); +} diff --git a/examples/posix-simple-cpp/Makefile b/examples/posix-simple-cpp/Makefile index df6051c..698870e 100644 --- a/examples/posix-simple-cpp/Makefile +++ b/examples/posix-simple-cpp/Makefile @@ -13,7 +13,9 @@ CXX = g++ CFLAGS = -Wall -Wextra -I../../src -Os CXXFLAGS = -std=c++17 -Wall -Wextra -I../../src -Os LDFLAGS = -lncurses -lm -TARGET = xelp-example-cpp + +BUILD_DIR ?= build +TARGET = $(BUILD_DIR)/xelp-example-cpp .PHONY: all build run clean @@ -21,17 +23,20 @@ all: build run build: $(TARGET) -xelp.o: ../../src/xelp.c ../../src/xelp.h ../../src/xelpcfg.h +$(BUILD_DIR)/xelp.o: ../../src/xelp.c ../../src/xelp.h ../../src/xelpcfg.h + @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) -c ../../src/xelp.c -o $@ -xelp-example-cpp.o: xelp-example-cpp.cpp ../../src/xelp.h ../../src/xelpcfg.h ../../src/XelpArduino.h +$(BUILD_DIR)/xelp-example-cpp.o: xelp-example-cpp.cpp ../../src/xelp.h ../../src/xelpcfg.h ../../src/XelpArduino.h + @mkdir -p $(BUILD_DIR) $(CXX) $(CXXFLAGS) -c xelp-example-cpp.cpp -o $@ -$(TARGET): xelp.o xelp-example-cpp.o - $(CXX) xelp.o xelp-example-cpp.o $(LDFLAGS) -o $@ +$(TARGET): $(BUILD_DIR)/xelp.o $(BUILD_DIR)/xelp-example-cpp.o + @mkdir -p $(BUILD_DIR) + $(CXX) $(BUILD_DIR)/xelp.o $(BUILD_DIR)/xelp-example-cpp.o $(LDFLAGS) -o $@ run: $(TARGET) ./$(TARGET) clean: - rm -f $(TARGET) *.o + rm -rf $(BUILD_DIR) diff --git a/examples/posix-simple/Makefile b/examples/posix-simple/Makefile index 3fffd29..599f608 100644 --- a/examples/posix-simple/Makefile +++ b/examples/posix-simple/Makefile @@ -9,7 +9,9 @@ CC = gcc CFLAGS = -Wall -Wextra -I../../src -Os LDFLAGS = -lncurses -lm SRC = xelp-example.c ../../src/xelp.c -TARGET = xelp-example + +BUILD_DIR ?= build +TARGET = $(BUILD_DIR)/xelp-example .PHONY: all build run clean @@ -18,10 +20,11 @@ all: build run build: $(TARGET) $(TARGET): $(SRC) ../../src/xelp.h ../../src/xelpcfg.h + @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(SRC) $(LDFLAGS) -o $@ run: $(TARGET) ./$(TARGET) clean: - rm -f $(TARGET) + rm -rf $(BUILD_DIR) diff --git a/examples/scripting/Makefile b/examples/scripting/Makefile index 77575bc..4a3a851 100644 --- a/examples/scripting/Makefile +++ b/examples/scripting/Makefile @@ -8,7 +8,9 @@ CC = gcc CFLAGS = -Wall -Wextra -I../../src SRC = scripting-example.c ../../src/xelp.c -TARGET = scripting-example + +BUILD_DIR ?= build +TARGET = $(BUILD_DIR)/scripting-example .PHONY: all build run clean @@ -17,10 +19,11 @@ all: build run build: $(TARGET) $(TARGET): $(SRC) ../../src/xelp.h ../../src/xelpcfg.h + @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(SRC) -o $@ run: $(TARGET) ./$(TARGET) clean: - rm -f $(TARGET) + rm -rf $(BUILD_DIR) diff --git a/makefile b/makefile index f77dc1a..968ee91 100755 --- a/makefile +++ b/makefile @@ -13,14 +13,14 @@ BUILD_DIR=build INCLUDES=\ -I$(LIB_DIR)\ -.PHONY: help tests clean clean-all coverage version fuzz fuzz-parsekey fuzz-parse examples example validate prerelease funcsizes sizes +.PHONY: help tests clean clean-all coverage version fuzz fuzz-parsekey fuzz-parse examples example validate prerelease funcsizes sizes lint #======================================================================= # Default target: print available targets help: @echo "xelp build targets:" @echo "" - @echo " make validate Build + run tests + build examples (pre-push check)" + @echo " make validate Lint + build + run tests + build examples (pre-push check)" @echo " make prerelease Validate + cross-build sizes + update README tables" @echo " make tests Build + run unit tests with coverage" @echo " make examples Build all examples (no interactive launch)" @@ -30,6 +30,7 @@ help: @echo " make sizes Feature profile compiled sizes (ARM + host)" @echo " make version Extract and print library version" @echo " make fuzz Run fuzz tests (requires clang + libFuzzer)" + @echo " make lint Run cppcheck static analysis on src + examples" @echo " make clean Remove test build artifacts" @echo " make clean-all Remove all build artifacts including examples" @echo "" @@ -79,24 +80,47 @@ version: examples: @echo "--- Building posix-simple ---" - $(MAKE) -C examples/posix-simple build + $(MAKE) -C examples/posix-simple BUILD_DIR=../../build/examples/posix-simple build @echo "--- Building posix-simple-cpp ---" - $(MAKE) -C examples/posix-simple-cpp build + $(MAKE) -C examples/posix-simple-cpp BUILD_DIR=../../build/examples/posix-simple-cpp build @echo "--- Building scripting ---" - $(MAKE) -C examples/scripting build + $(MAKE) -C examples/scripting BUILD_DIR=../../build/examples/scripting build @echo "--- All examples built ---" # Build and run the posix ncurses demo (interactive) example: - $(MAKE) -C examples/posix-simple + $(MAKE) -C examples/posix-simple BUILD_DIR=../../build/examples/posix-simple #======================================================================= -# Local validation: tests + examples build (no Docker, no release) +# Static analysis with cppcheck + +lint: + @if command -v cppcheck >/dev/null 2>&1; then \ + echo "--- cppcheck: src + examples (C) ---"; \ + cppcheck --enable=warning,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + -I src \ + src/ examples/posix-simple/ examples/scripting/; \ + echo "--- cppcheck: examples (C++) ---"; \ + cppcheck --enable=warning,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + -I src \ + --language=c++ \ + examples/posix-simple-cpp/xelp-example-cpp.cpp; \ + echo "--- cppcheck passed ---"; \ + else \ + echo "--- cppcheck not found, skipping lint (install: brew/apt install cppcheck) ---"; \ + fi + +#======================================================================= +# Local validation: lint + tests + examples build (no Docker, no release) # Use this for day-to-day development before pushing. -validate: tests examples +validate: lint tests examples @echo "" - @echo "=== Validation passed: tests + examples build clean ===" + @echo "=== Validation passed: lint + tests + examples build clean ===" #======================================================================= # Pre-release: validate + cross-compile sizes + update README tables @@ -152,7 +176,7 @@ clean: # clean-all -- clean tests + all examples clean-all: clean - -$(MAKE) -C examples/posix-simple clean - -$(MAKE) -C examples/posix-simple-cpp clean - -$(MAKE) -C examples/scripting clean + -$(MAKE) -C examples/posix-simple BUILD_DIR=../../build/examples/posix-simple clean + -$(MAKE) -C examples/posix-simple-cpp BUILD_DIR=../../build/examples/posix-simple-cpp clean + -$(MAKE) -C examples/scripting BUILD_DIR=../../build/examples/scripting clean diff --git a/tools/make_release.sh b/tools/make_release.sh index 69b307f..0c3f599 100755 --- a/tools/make_release.sh +++ b/tools/make_release.sh @@ -381,15 +381,48 @@ do_push_branch() { echo " Branch is $behind commit(s) behind origin/master." echo " Merging origin/master..." if ! run_cmd git merge origin/master --no-edit; then - fail "Merge conflict. Resolve manually and re-run. + # Check if all conflicted files are pipeline-managed. + # If so, auto-resolve by keeping ours (the feature branch + # already has the correct version from do_sync_manifests). + local conflicted + conflicted=$(git diff --name-only --diff-filter=U) + local all_known=true + while IFS= read -r cfile; do + [ -z "$cfile" ] && continue + local found=false + for known in $PIPELINE_FILES; do + if [ "$cfile" = "$known" ]; then + found=true + break + fi + done + if ! $found; then + all_known=false + break + fi + done <<< "$conflicted" + + if $all_known && [ -n "$conflicted" ]; then + echo " Auto-resolving pipeline file conflicts (keeping ours)..." + while IFS= read -r cfile; do + [ -z "$cfile" ] && continue + run_cmd git checkout --ours "$cfile" + run_cmd git add "$cfile" + done <<< "$conflicted" + run_cmd git commit --no-edit + pass "Auto-resolved pipeline file conflicts and completed merge." + else + fail "Merge conflict in non-pipeline files. Resolve manually and re-run. Commands to resolve: git status # see conflicted files # ... edit and fix conflicts ... git add git commit bash tools/make_release.sh # re-run" + fi + else + pass "Merged origin/master into $BRANCH." fi - pass "Merged origin/master into $BRANCH." fi fi @@ -910,25 +943,15 @@ if [ "$MODE" = "validate" ]; then fi # -- Full release flow -- +do_sync_manifests +do_update_badges do_crossbuild do_commit_pipeline_changes do_check_git -do_sync_manifests -do_update_badges - -# Auto-commit manifest and badge updates (if any files were staged) -if [ -n "$(git diff --cached --name-only)" ]; then - step_header "Commit manifest and badge updates" - run_cmd git commit -m "Sync manifests and badges for $VER_STRING" - pass "Committed version sync changes." -fi if $TAG_EXISTS; then # Re-run: tag exists but release doesn't -- just wait for it do_wait_release - do_pio_publish - do_idf_publish - do_done else do_push_branch do_open_pr @@ -938,7 +961,16 @@ else do_verify_master do_tag do_wait_release +fi + +# Publish to package registries +if [ "$MODE" = "release-local" ]; then do_pio_publish do_idf_publish - do_done +else + echo "" + echo " PIO and ESP-IDF publishing is handled by CI (release.yml)." + echo " To publish locally instead, re-run with --release-local." fi + +do_done From 81721beb22ad113151f6c91abf36bac6a73eb39f Mon Sep 17 00:00:00 2001 From: deftio Date: Mon, 11 May 2026 04:22:27 -0700 Subject: [PATCH 2/3] updated examples, BLE and serial talking via separate instances --- AGENTS.md | 798 +++++++++++++++--- CHANGELOG.md | 25 + README.md | 6 +- dev/manu_xelp_notes_legacy.md | 4 + docs/README.md | 5 + docs/examples.md | 78 ++ examples/arduino-cpp/README.md | 18 + examples/arduino-live-cli/README.md | 62 +- .../arduino-live-cli/arduino-live-cli.ino | 584 ++++++++----- examples/arduino/README.md | 18 + examples/esp32-ble-cli/Makefile | 74 ++ examples/esp32-ble-cli/README.md | 140 +++ examples/esp32-ble-cli/XelpArduino.h | 1 + examples/esp32-ble-cli/esp32-ble-cli.ino | 589 +++++++++++++ examples/esp32-ble-cli/xelp.c | 1 + examples/esp32-ble-cli/xelp.h | 1 + examples/esp32-ble-cli/xelpcfg.h | 1 + examples/esp32-wifi/README.md | 18 + examples/esp32c6-wifi/web/index.html | 6 +- examples/pico-cli-arduino/README.md | 18 + examples/pico-cli/README.md | 9 +- llms.txt | 140 ++- pages/examples.html | 89 ++ pages/index.html | 2 + src/xelp.c | 26 +- src/xelpcfg.h | 117 ++- 26 files changed, 2441 insertions(+), 389 deletions(-) create mode 100644 examples/esp32-ble-cli/Makefile create mode 100644 examples/esp32-ble-cli/README.md create mode 120000 examples/esp32-ble-cli/XelpArduino.h create mode 100644 examples/esp32-ble-cli/esp32-ble-cli.ino create mode 120000 examples/esp32-ble-cli/xelp.c create mode 120000 examples/esp32-ble-cli/xelp.h create mode 120000 examples/esp32-ble-cli/xelpcfg.h diff --git a/AGENTS.md b/AGENTS.md index 28de466..9b0c776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,32 +1,94 @@ # AGENTS.md -- AI Coding Agent Reference for xelp This file provides the essential context an AI coding agent needs to -generate correct xelp code. Read this instead of the full source when -integrating xelp into a project. +generate correct xelp code, write tests, port to new platforms, and +understand the internal architecture. Read this instead of the full +source when integrating xelp into a project. + +For the llmstxt.org-format project index, see [llms.txt](llms.txt). ## What xelp is -A command line interpreter and script engine for embedded systems. Pure C, -zero dynamic memory, no OS, no stdlib. Three files: `xelp.c`, `xelp.h`, -`xelpcfg.h`. Compiles on 8-bit through 64-bit targets with any C89+ -compiler. +A command line interpreter, script engine, and single-key menu system +for embedded systems. Pure C, zero dynamic memory, no OS, no stdlib. +Three files: `xelp.c`, `xelp.h`, `xelpcfg.h`. Compiles on 8-bit +through 64-bit targets with any C89+ compiler. Version 0.3.2. ## Critical constraints - **No malloc, no heap.** Never suggest `malloc`, `calloc`, `strdup`, `asprintf`, or any dynamic allocation in xelp-related code. - **No stdlib string functions.** xelp provides its own: `XelpStrLen`, - `XelpStrEq`, `XelpStr2Int`, `XelpParseNum`. Do not use `strlen`, - `strcmp`, `atoi`, `strtol`, or `sprintf` unless the user's platform - already links them. + `XelpStrEq`, `XelpStrEq2`, `XelpStr2Int`, `XelpParseNum`, + `XelpBufCmp`. Do not use `strlen`, `strcmp`, `atoi`, `strtol`, or + `sprintf` unless the user's platform already links them. - **No printf.** xelp outputs one character at a time through a user-supplied `void (*)(char)` function. Use `XelpOut()` to print - strings. -- **Strings stored by pointer.** Prompt strings and about messages passed - to `XELP_SET_VAL_CLI_PROMPT()` and `XELP_SET_ABOUT()` are stored by - pointer, not copied. They must be null-terminated and must remain valid - for the life of the instance (string literals, static buffers, or - globals -- never stack-local buffers that go out of scope). + strings. Use `XelpPutc()` for single characters. +- **Strings stored by pointer.** Prompt strings and about messages + passed to `XELP_SET_VAL_CLI_PROMPT()` and `XELP_SET_ABOUT()` are + stored by pointer, not copied. They must be null-terminated and must + remain valid for the life of the instance (string literals, static + buffers, or globals -- never stack-local buffers that go out of scope). +- **C89/C90 compatibility.** No `//` comments in `src/` files. No + mixed declarations and code. No VLAs. No GCC extensions. +- **No globals.** All state goes through the `XELP *ths` instance + pointer. Multiple instances must run independently. + +--- + +## Architecture overview + +``` + xelp architecture + ================= + + Hardware byte stream (UART RX, BLE, USB CDC, etc.) + | + v + XelpParseKey(ths, char) <-- interactive: one byte at a time + | + +-- _xelpKeyAccum() <-- assembles multi-byte ANSI sequences + | (ESC [ A -> XELP_KEYCODE_UP, etc.) + | + +-- mode switch check <-- ESC/CTRL-P/CTRL-T + | + +-- dispatch by mode: + | + +-- KEY mode: XelpExecKC() -> key table lookup -> handler(ths, keycode) + | + +-- THR mode: mpfPassThru(char) + | + +-- CLI mode: line buffer -> on ENTER: + | + +-- _xelpHistSave() (optional, if XELP_ENABLE_HISTORY) + | + +-- XelpParseXB() <-- tokenize + dispatch + | + +-- XelpTokLineXB() <-- PSM state machine tokenizer + | + +-- command table linear search + | + +-- handler(ths, args, len) + + XelpParse(ths, buf, len) <-- scripting: parse entire buffer at once + | + +-- wraps XelpParseXB() <-- same tokenizer + dispatch pipeline +``` + +## Three modes + +| Mode | Constant | Description | Compile flag | +|------|----------|-------------|-------------| +| CLI | `XELP_MODE_CLI` (0x00) | Line-buffered input with prompt. Type commands, press ENTER. | `XELP_ENABLE_CLI` | +| KEY | `XELP_MODE_KEY` (0x01) | Each keypress triggers a command immediately. For menus. | `XELP_ENABLE_KEY` | +| THR | `XELP_MODE_THR` (0x02) | All keys pass through to another peripheral. | `XELP_ENABLE_THR` | + +Default mode switch keys: **ESC** -> KEY, **CTRL-P** -> CLI, +**CTRL-T** -> THR. Configurable in `xelpcfg.h`. If a mode is not +compiled in, its switch key is silently ignored. + +--- ## Function signatures (v0.3.2+) @@ -35,7 +97,7 @@ compiler. ```c XELPRESULT my_command(XELP *ths, const char *args, int len) { /* ths -- the invoking xelp instance (use for XelpOut, registers, etc.) - args -- raw argument string (not null-terminated, use len) + args -- raw argument string including command name (not null-terminated, use len) len -- length of args in bytes */ XelpOut(ths, "Hello\n", 0); return XELP_S_OK; @@ -57,8 +119,10 @@ XELPRESULT my_key_handler(XELP *ths, XELPKEYCODE key) { Multi-byte key constants: `XELP_KEYCODE_UP`, `XELP_KEYCODE_DOWN`, `XELP_KEYCODE_LEFT`, `XELP_KEYCODE_RIGHT`, `XELP_KEYCODE_HOME`, -`XELP_KEYCODE_END`, `XELP_KEYCODE_INS`, `XELP_KEYCODE_KDEL`. +`XELP_KEYCODE_END`, `XELP_KEYCODE_INS`, `XELP_KEYCODE_KDEL`, +`XELP_KEYCODE_PGUP`, `XELP_KEYCODE_PGDN`. Single-char keys are their natural value (`'?'` == 0x3F). +`XELP_KC_IS_MULTI(k)` returns true if the keycode is multi-byte (>= 0x100). ### Command tables @@ -80,6 +144,27 @@ XELPKeyFuncMapEntry key_commands[] = { **Note:** v0.2.x used different signatures without the `XELP *ths` parameter. All current code must use the v0.3.0+ signatures shown above. +### Default handlers + +Called when no command/key matches. Optional. + +```c +XELPRESULT default_cli(XELP *ths, const char *args, int len) { + XelpOut(ths, "Unknown command\n", 0); + return XELP_E_CMDNOTFOUND; +} + +XELPRESULT default_key(XELP *ths, XELPKEYCODE key) { + (void)key; + return XELP_S_NOTFOUND; +} + +XELP_SET_FN_DEF_CLI(cli, &default_cli); +XELP_SET_FN_DEF_KEY(cli, &default_key); +``` + +--- + ## Setup pattern Every xelp integration follows this pattern: @@ -106,36 +191,41 @@ void loop(void) { } ``` -## Parsing arguments inside commands (XelpArgs -- preferred) +### All setup macros -For sequential left-to-right argument parsing, use `XelpArgs`: +| Macro | Purpose | +|-------|---------| +| `XELP_SET_FN_OUT(ths, fn)` | Set output function: `void fn(char)` **(required)** | +| `XELP_SET_FN_ERR(ths, fn)` | Set error output function (optional) | +| `XELP_SET_FN_BKSP(ths, fn)` | Set backspace handler: `void fn(void)` | +| `XELP_SET_FN_THR(ths, fn)` | Set pass-through function | +| `XELP_SET_FN_EMCHG(ths, fn)` | Set mode-change callback: `void fn(int)` | +| `XELP_SET_FN_CLI(ths, tbl)` | Set CLI command table | +| `XELP_SET_FN_KEY(ths, tbl)` | Set KEY command table | +| `XELP_SET_FN_DEF_CLI(ths, fn)` | Set default CLI handler | +| `XELP_SET_FN_DEF_KEY(ths, fn)` | Set default KEY handler | +| `XELP_SET_VAL_CLI_PROMPT(ths, str)` | Set prompt (stored by pointer) | +| `XELP_SET_ABOUT(ths, str)` | Set about/help message (stored by pointer) | + +--- + +## Parsing arguments inside commands + +### XelpArgs -- sequential iterator (preferred) + +For sequential left-to-right argument parsing, use `XelpArgs`. +O(1) per token. Preferred over `XelpTokN`. ```c XELPRESULT cmd_set(XELP *ths, const char *args, int len) { XelpArgs a; + XelpBuf key; int value; XelpArgsInit(&a, args, len); XelpNextTok(&a, 0); /* skip command name ("set") */ - XelpNextTok(&a, 0); /* skip key (or read it into a XelpBuf) */ + XelpNextTok(&a, &key); /* key: use key.s .. key.p */ XelpNextInt(&a, &value); - return XELP_S_OK; -} -``` - -## Parsing arguments with XelpArgs (full reference) - -XelpArgs is a sequential iterator that yields one token at a time in O(1). -Preferred over `XelpTokN` when arguments are processed left-to-right: - -```c -XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { - XelpArgs a; - int dividend, divisor; - XelpArgsInit(&a, args, len); - XelpNextTok(&a, 0); /* skip command name */ - XelpNextInt(&a, ÷nd); - XelpNextInt(&a, &divisor); - /* ... */ + /* ... use key and value ... */ return XELP_S_OK; } ``` @@ -143,15 +233,16 @@ XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { | Function | Purpose | |----------|---------| | `XelpArgsInit(a, args, len)` | Initialize iterator from `const char *args` | -| `XelpNextTok(a, &tok)` | Get next token as `XelpBuf` (pass `0` to skip) | +| `XelpNextTok(a, &tok)` | Get next token as `XelpBuf` (pass `NULL` or `0` to skip) | | `XelpNextInt(a, &val)` | Get next token and parse as int | -| `XelpArgCount(a, &n)` | Count remaining tokens (does not consume) | +| `XelpArgCount(a, &n)` | Count total tokens (does not consume; preserves position) | -Tokens are NOT null-terminated. Use `tok.s`..`tok.p` or `XELP_XB_LEN(tok)`. +Tokens are NOT null-terminated. Use `tok.s`..`tok.p` span or +`XELP_XB_LEN(tok)` for length. -## Direct argument access (XelpArgInt / XelpArgStr) +### Direct argument access (XelpArgInt / XelpArgStr) -For one-shot access to a specific argument by index: +For one-shot access to a specific argument by index (O(N) per call): ```c XELPRESULT cmd_set(XELP *ths, const char *args, int len) { @@ -185,6 +276,8 @@ XELPRESULT cmd_get(XELP *ths, const char *args, int len) { } ``` +--- + ## Running scripts Scripts are const strings parsed without modification (ROM-safe): @@ -195,6 +288,21 @@ XelpParse(&cli, script, XelpStrLen(script)); ``` Separators: `;` and `\n`. Comments: `#` to end of line. +Quoted strings: `"hello world"` is a single token. +Escape chars: backtick (`` ` ``) at CLI, backslash (`\`) inside quotes. + +Multi-line scripts: + +```c +const char *script = + "# startup configuration\n" + "set mode 1\n" + "set gain 50\n" + "echo config complete\n"; +XelpParse(&cli, script, XelpStrLen(script)); +``` + +--- ## Output from commands @@ -214,6 +322,8 @@ characters. If `maxlen <= 0`, prints until null terminator. `XelpPutc(ths, c)`: single-character output through the instance's output function. Respects `mOutEnable`. Use instead of `XelpOut` for single chars. +--- + ## Multiple instances xelp uses no global state. Each instance is independent: @@ -233,6 +343,8 @@ XELP_SET_FN_CLI(cli_serial, commands); XELP_SET_FN_CLI(cli_ble, commands); ``` +--- + ## Return codes | Constant | Value | Meaning | @@ -246,50 +358,16 @@ XELP_SET_FN_CLI(cli_ble, commands); `XELP_T_OK(r)` returns true if `r >= 0`. -## Compile-time configuration - -Edit `src/xelpcfg.h` to enable/disable features: - -| Flag | Purpose | Size impact | -|--------------------|----------------------------------|----------------| -| `XELP_ENABLE_CLI` | CLI mode + scripting | Core (~2 KB) | -| `XELP_ENABLE_LINE_EDIT` | Cursor movement, insert, delete | ~800-1000 bytes | -| `XELP_ENABLE_KEY` | Single keypress mode | ~200-500 bytes | -| `XELP_ENABLE_THR` | Pass-through mode | ~50-125 bytes | -| `XELP_ENABLE_HELP` | Built-in help command | ~180-350 bytes | -| `XELP_ENABLE_HISTORY` | Command history (UP/DOWN recall) | ~420 bytes | -| `XELP_ENABLE_FULL` | All of the above | All combined | - -Buffer size: `XELP_CMDBUFSZ` (default 64 bytes). -History depth: `XELP_HIST_DEPTH` (default 4 commands). Override at compile time or via `XELP_CONFIG_OVERRIDE`. - -## Three modes - -- **CLI** (default): Line-buffered input with prompt. Type commands, press ENTER. -- **KEY**: Each keypress triggers a command immediately. For menus. -- **THR**: All keys pass through to another peripheral. - -Default mode switch keys: ESC (KEY), CTRL-P (CLI), CTRL-T (THR). - -## Common mistakes to avoid - -1. Forgetting `XELP *ths` as the first parameter of command functions. -2. Using `printf` or `puts` instead of `XelpOut(ths, ...)`. -3. Passing stack-local strings to `XELP_SET_VAL_CLI_PROMPT()`. -4. Forgetting `XELP_FUNC_ENTRY_LAST` at the end of command tables. -5. Treating `args` as null-terminated (it is not -- use `len`). -6. Hardcoding `&global_instance` instead of using `ths` in commands. -7. Calling `malloc` or stdlib functions in embedded contexts. +--- ## Registers (v0.3.2+) Each XELP instance has 4 return registers (`mR[0..3]`), accessed via macros. Convention: **callee-clobbers-all** -- any command call may -overwrite all registers. These are a return-value mailbox, NOT -computational registers (the VM has its own register file). +overwrite all registers. These are a return-value mailbox. ```c -/* Read after a command call */ +/* Read after a command call (struct access via macros) */ XELPREG status = XELP_R0(cli); /* engine writes: XELP_S_OK, etc. */ XELPREG val1 = XELP_R1(cli); /* command-specific return value 1 */ XELPREG val2 = XELP_R2(cli); /* command-specific return value 2 */ @@ -306,20 +384,25 @@ XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { C++ wrapper: `cli.r0()` (read-only), `cli.r1()`-`cli.r3()` (read/write). -## Output Control +--- + +## Output control Two `char` fields in the XELP struct control output behavior: -- **`mOutEnable`** -- gates ALL output (XelpOut, echo, prompt, help). - Set via `XELP_SET_OUT_ENABLE(ths, val)`. Default: 1 (enabled). - Set to 0 for silent scripting / batch mode. +### Output enable (`mOutEnable`) + +Gates **ALL** output (XelpOut, echo, prompt, help, redraw). +Set via `XELP_SET_OUT_ENABLE(ths, val)`. Default: 1 (enabled). +Set to 0 for silent scripting / batch mode. -- **`mEchoChar`** -- controls how printable chars are echoed during - interactive input. Does NOT affect XelpOut, ENTER newline, cursor - movement, or prompt. - - `XELP_ECHO_NORMAL` (`'\0'`) -- echo as typed (default) - - `XELP_ECHO_OFF` (`'\1'`) -- suppress echo - - Any other char (e.g. `'*'`) -- mask: echo that char instead +### Echo control (`mEchoChar`) + +Controls how printable chars are echoed during interactive input. +Does NOT affect XelpOut, ENTER newline, cursor movement, or prompt. +- `XELP_ECHO_NORMAL` (`'\0'`) -- echo as typed (default) +- `XELP_ECHO_OFF` (`'\1'`) -- suppress echo +- Any other char (e.g. `'*'`) -- mask: echo that char instead Password entry pattern: ```c @@ -328,12 +411,539 @@ XELP_SET_ECHO(*ths, '*'); /* mask during input */ XELP_SET_ECHO(*ths, XELP_ECHO_NORMAL); /* restore after ENTER */ ``` +--- + +## Compile-time configuration + +Edit `src/xelpcfg.h` to enable/disable features: + +| Flag | Purpose | Size impact | +|------|---------|-------------| +| `XELP_ENABLE_CLI` | CLI mode + scripting | Core (~1.5-2.6 KB) | +| `XELP_ENABLE_LINE_EDIT` | Cursor movement, insert, delete | ~800-1000 bytes | +| `XELP_ENABLE_HISTORY` | Command history (UP/DOWN recall) | ~420 bytes | +| `XELP_ENABLE_KEY` | Single keypress mode | ~200-500 bytes | +| `XELP_ENABLE_THR` | Pass-through mode | ~50-125 bytes | +| `XELP_ENABLE_HELP` | Built-in help command | ~180-350 bytes | +| `XELP_ENABLE_FULL` | All of the above (except history) | All combined | + +### Buffer and register sizes + +| Setting | Default | Purpose | +|---------|---------|---------| +| `XELP_CMDBUFSZ` | 64 | CLI input buffer size (bytes) | +| `XELP_REGS_SZ` | 4 | Return registers per instance (minimum 4) | +| `XELPREG` | `int` | Register element type | +| `XELP_HIST_DEPTH` | 4 | History ring buffer depth | + +### Key mappings + +| Setting | Default | Purpose | +|---------|---------|---------| +| `XELPKEY_CLI` | CTRL-P | Switch to CLI mode | +| `XELPKEY_KEY` | ESC | Switch to KEY mode | +| `XELPKEY_THR` | CTRL-T | Switch to THR mode | + +### Escape characters + +| Setting | Default | Purpose | +|---------|---------|---------| +| `XELP_CLI_ESC` | `` ` `` (backtick) | Escape next char in CLI/scripts | +| `XELP_QUO_ESC` | `\` (backslash) | Escape next char inside quoted strings | + +### ENTER key detection + +| Setting | Default | Purpose | +|---------|---------|---------| +| `XELP_ENTER_LF` | 1 | Accept `\n` (0x0A) as ENTER | +| `XELP_ENTER_CR` | 1 | Accept `\r` (0x0D) as ENTER | + +Both enabled by default for cross-platform use. Only affects interactive +input (`XelpParseKey`); script parsing always uses `\n`. + +### Prompt configuration + +```c +/* Fixed prompt (all instances share it) -- default */ +#define XELP_CLI_PROMPT "xelp>" + +/* Per-instance prompt */ +#define XELP_CLI_PROMPT (ths->mpPrompt) +/* Then use: XELP_SET_VAL_CLI_PROMPT(cli, "mydev>"); */ +``` + +### Help string customization + +| Setting | Default | Purpose | +|---------|---------|---------| +| `XELP_HELP_KEY_STR` | `"\nKey functions\n"` | Section header for KEY commands | +| `XELP_HELP_CLI_STR` | `"\nCLI functions\n"` | Section header for CLI commands | +| `XELP_HELP_ABT_STR` | `(ths->mpAboutMsg)` | About message at top of help | + +### Configuration override system + +To customize without modifying source files: + +1. Define `-DXELP_CONFIG_OVERRIDE` in compiler flags +2. Create `xelp_ovr.h` in your include path +3. Use `#undef` / `#define` to change values + +```c +/* xelp_ovr.h example */ +#undef XELP_CLI_PROMPT +#define XELP_CLI_PROMPT (ths->mpPrompt) + +#undef XELP_ENABLE_THR /* disable THR mode */ + +#undef XELP_HIST_DEPTH +#define XELP_HIST_DEPTH 8 /* 8 history entries instead of 4 */ +``` + +--- + +## Parser internals + +### PSM (Parser State Machine) + +The tokenizer is a table-driven finite state machine in `xelp.c`. It +operates on the input buffer without modifying it (ROM-safe). + +**8 states:** + +| State | ID | Description | +|-------|----|-------------| +| `_PS_SEEK` | 0x00 | Seeking next token, skipping whitespace | +| `_PS_ESCA` | 0x01 | CLI escape sequence (backtick) | +| `_PS_TOK0` | 0x02 | Inside first token (command name) | +| `_PS_CMNT` | 0x03 | Inside single-line comment | +| `_PS_SEOL` | 0x04 | Seeking end-of-line after first token | +| `_PS_QUOT` | 0x05 | Inside quoted string | +| `_PS_QESC` | 0x06 | Escape char inside quoted string | +| `_PS_QEND` | 0x07 | Exiting quoted string | + +**State table:** `gPSMStates[94]` -- each entry is 3 bytes: +`[char_to_match, exec_flags, next_state]`. Char 0 = default for state. + +**Jump table:** `gPSMJumpTable[8]` -- maps state ID to offset in +`gPSMStates` for O(1) state lookup. + +**Execution flags:** +- `_EF_TS` (0x01) -- mark token start +- `_EF_TE` (0x02) -- mark token end +- `_EF_LE` (0x04) -- mark line end + +### Tokenizer function: `XelpTokLineXB()` + +Two modes: +- `XELP_TOK_ONLY` -- returns next single token. Output: `tok.s` = start, + `tok.p` = end of token. +- `XELP_TOK_LINE` -- returns entire line. Output: `tok.s` = command + start, `tok.p` = command end, `tok.e` = end of line (including args). + +### Key accumulator: `_xelpKeyAccum()` + +Separate from the PSM. Assembles raw bytes into `XELPKEYCODE` values: +- Single chars complete immediately +- ESC triggers look-ahead: ESC + `[` enters CSI mode +- CSI sequences collect digits and terminate on letter or `~` +- Keycodes packed little-endian into `unsigned long` + +### Command dispatch + +`XelpParseXB()` loop: +1. Call `XelpTokLineXB()` with `XELP_TOK_LINE` to get next command line +2. Linear search through `mpCLIModeFuncs` table +3. First match (via `XelpStrEq2`) calls the handler with the full line +4. If no match and `mpfDefCLI` is set, call the default handler +5. Result stored in `ths->mR[0]` + +--- + +## XelpBuf -- buffer wrapper + +```c +typedef struct { + char* s; /* start of buffer */ + char* p; /* current position / cursor */ + char* e; /* end of buffer (s + length) */ +} XelpBuf; +``` + +Invariant: `s <= p <= e`. + +| Macro | Purpose | +|-------|---------| +| `XELP_XB_INIT(xb, buf, len)` | Init from pointer + length | +| `XELP_XB_INIT_PTRS(xb, s, p, e)` | Init from three pointers | +| `XELP_XB_INIT_BP(xb, buf, pos, len)` | Init with cursor offset | +| `XELP_XB_COPY(a, b)` | Copy a to b | +| `XELP_XB_PTR(xb)` | Get start pointer | +| `XELP_XB_LEN(xb)` | Total buffer length | +| `XELP_XB_POS(xb)` | Current position as int | +| `XELP_XB_PUTC(xb, ch)` | Write char with bounds check | +| `XELP_XB_PUTC_RAW(xb, ch)` | Write char without bounds check | +| `XELP_XB_GETC(xb, ch)` | Read char and advance | +| `XELP_XB_TOP(xb)` | Reset position to start | +| `XELP_XB_OUT(x, xb)` | Print XelpBuf via XelpOut | + +There is also a `XelpBufC` (const variant) with `const char*` members. +`XelpBuf` is an alias for `XelpBufW` (writable). + +--- + +## String utilities + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `XelpStrLen(c)` | `int (const char*)` | Length of null-terminated string | +| `XelpStrEq(buf, len, cmd)` | `XELPRESULT (const char*, int, const char*)` | Compare buffer to null-terminated string | +| `XelpStrEq2(buf, end, cmd)` | `XELPRESULT (const char*, const char*, const char*)` | Compare pointer-pair to null-terminated string | +| `XelpStr2Int(s, maxlen)` | `int (const char*, int)` | Parse string to int (returns 0 on error) | +| `XelpParseNum(s, maxlen, &n)` | `XELPRESULT (const char*, int, int*)` | Safer parse: returns result code | +| `XelpBufCmp(as, ae, bs, be, type)` | `XELPRESULT (...)` | Compare two buffers with null-term options | +| `XelpFindTok(x, t0s, t0e, type)` | `XELPRESULT (XelpBuf*, ...)` | Find matching token in buffer | + +### Number parsing + +`XelpParseNum` and `XelpStr2Int` accept: +- Decimal: `123`, `-45`, `+67` +- Hex prefix: `0xFF`, `0x1A` +- Hex suffix: `FFh`, `1Ah` +- Uppercase and lowercase hex digits + +Overflow detection is included (returns `XELP_E_ERR` on overflow). + +### Buffer comparison types + +| Constant | Value | Behavior | +|----------|-------|----------| +| `XELP_CMP_TYPE_BUF` | 0x00 | Pure byte comparison, `\0` not special | +| `XELP_CMP_TYPE_A0` | 0x01 | Buffer A terminates on `\0` | +| `XELP_CMP_TYPE_A0B0` | 0x11 | Both buffers terminate on `\0` | + +--- + +## XELP instance struct + +Key fields (see `xelp.h` for full definition): + +```c +typedef struct XELP_tag { + int mCurMode; /* CLI / KEY / THR */ + char mOutEnable; /* 0=mute, nonzero=normal */ + char mEchoChar; /* '\0'=normal, '\1'=off, else mask */ + const char* mpAboutMsg; /* help header string */ + XELPREG mR[XELP_REGS_SZ]; /* return registers */ + + /* CLI mode (if XELP_ENABLE_CLI) */ + XELPCLIFuncMapEntry *mpCLIModeFuncs; + char mCmdMsgBuf[XELP_CMDBUFSZ]; + XelpBuf mCmdXB; + + /* KEY mode (if XELP_ENABLE_KEY) */ + XELPKeyFuncMapEntry *mpKeyModeFuncs; + + /* Key accumulator */ + XELPKEYCODE mKeyAccum; + char mKeyLen; + + /* Line editing (if XELP_ENABLE_LINE_EDIT) */ + char* mCur; /* cursor in [mCmdXB.s .. mCmdXB.p] */ + + /* History (if XELP_ENABLE_HISTORY) */ + char mHistBuf[XELP_HIST_DEPTH][XELP_CMDBUFSZ]; + char mHistWrite, mHistCount, mHistBrowse; + char mHistSaved[XELP_CMDBUFSZ]; + char mHistSavedLen; + + /* Platform abstraction */ + void (*mpfOut)(char); /* character output */ + void (*mpfErr)(char); /* error output */ + void (*mpfEditModeChg)(int); /* mode change callback */ + void (*mpfPassThru)(char); /* THR mode forward */ + void (*mpfBksp)(); /* destructive backspace */ +} XELP; +``` + +--- + +## Version information + +```c +#define XELP_VERSION (0x00000302UL) /* 0x00MMmmpp */ +#define XELP_VER_MAJOR(v) (((v) >> 16) & 0xFF) +#define XELP_VER_MINOR(v) (((v) >> 8) & 0xFF) +#define XELP_VER_PATCH(v) ( (v) & 0xFF) +``` + +The version in `src/xelp.h` is the single source of truth. + +--- + +## Common mistakes to avoid + +1. Forgetting `XELP *ths` as the first parameter of command functions. +2. Using `printf` or `puts` instead of `XelpOut(ths, ...)`. +3. Passing stack-local strings to `XELP_SET_VAL_CLI_PROMPT()`. +4. Forgetting `XELP_FUNC_ENTRY_LAST` at the end of command tables. +5. Treating `args` as null-terminated (it is not -- use `len`). +6. Hardcoding `&global_instance` instead of using `ths` in commands. +7. Calling `malloc` or stdlib functions in embedded contexts. +8. Using `//` comments in core source files (must use `/* */` for C89). +9. Mixed declarations and code in core source files. +10. Assuming `int` is a specific size (varies 16-bit to 64-bit). +11. Using `XELP_ENABLE_HISTORY` without `XELP_ENABLE_LINE_EDIT`. +12. Forgetting the `(char*)` cast in `XELP_XB_INIT(b, (char*)args, len)`. + +--- + +## C++ wrapper (XelpArduino.h) + +For Arduino and C++ projects, `src/XelpArduino.h` provides a `XelpCLI` +wrapper class with syntactic sugar: + +```cpp +#include "XelpArduino.h" + +XelpCLI cli; + +void setup() { + Serial.begin(115200); + cli.begin("My Device", &Serial); + cli.setCommands(commands); +} + +void loop() { + cli.poll(Serial); +} +``` + +Methods: `begin()`, `setCommands()`, `poll()`, `parse()`, +`r0()`-`r3()`, `help()`. + +--- + +## Testing + +### Test framework + +xelp uses **JumpBug**, a minimal C89-compatible unit test framework with +no external dependencies. Tests are in `tests/xelp_unit_tests.c`. + +**47 test units, 598 test cases, 100% line coverage of xelp.c.** + +### Running tests + +```bash +make validate # tests + build all examples (the everyday check) +make tests # unit tests + gcov coverage +make coverage # coverage summary +make fuzz # libFuzzer fuzz testing (requires clang) +``` + +### Writing a new test + +```c +XELPRESULT test_my_feature(JumpBug *jb) { + XELP cli; + char out_buf[256]; + int out_pos = 0; + + /* capture output */ + void capture(char c) { out_buf[out_pos++] = c; } + + XelpInit(&cli, "test"); + XELP_SET_FN_OUT(cli, &capture); + XELP_SET_FN_CLI(cli, test_commands); + + /* ... exercise the feature ... */ + + JB_ASSERT(jb, out_pos > 0, "should produce output"); + JB_ASSERT_EQ(jb, XELP_R0(cli), XELP_S_OK, "should succeed"); + + return JB_PASS; +} +``` + +Register in test runner: + +```c +JumpBug_RunUnit(&jb, &test_my_feature, "my feature"); +``` + +### Fuzz testing + +Harnesses in `tests/fuzz/`: +- `fuzz_parse.c` -- fuzzes `XelpParse` (script buffer) +- `fuzz_parsekey.c` -- fuzzes `XelpParseKey` (byte-by-byte input) + +Seed corpus in `tests/fuzz/corpus_parse/`. + +--- + +## Build system + +### Makefile targets + +```bash +make validate # tests + examples + zero warnings (everyday check) +make tests # unit tests + coverage +make examples # build all examples +make example # build and run posix ncurses demo (interactive) +make coverage # coverage summary +make fuzz # libFuzzer fuzz testing +make funcsizes # per-function compiled sizes +make sizes # feature profile sizes +make version # extract version as YAML +make prerelease # validate + Docker cross-compile + size table update +make clean # remove test artifacts +make clean-all # remove all artifacts including examples +``` + +### CMake + +```cmake +# As ESP-IDF component (auto-detected) +idf_component_register(SRCS "src/xelp.c" INCLUDE_DIRS "src") + +# As plain CMake library +add_library(xelp src/xelp.c) +target_include_directories(xelp PUBLIC src) +``` + +### Package manifests + +- `library.json` -- PlatformIO +- `library.properties` -- Arduino Library Manager +- `idf_component.yml` -- ESP-IDF Component Registry + +--- + +## Platform-specific notes + +### 8051 (SDCC) + +SDCC for 8051 requires `__reentrant` on function pointers that may be +called reentrantly. xelp handles this with the `REENTRANT_SDCC` macro. +No user action needed. + +### AVR / Arduino + +Flash-resident strings (`PROGMEM`) are not directly supported by xelp's +string functions. For ROM strings, use standard AVR pgm_read techniques +to copy to RAM before passing to xelp. + +### ARM bare metal + +ARM Thumb (`-mthumb`) produces the smallest code. xelp's primary size +baseline is ARM Cortex-M0 Thumb with `-Os`. + +### MSP430 + +16-bit `int` is fine -- xelp uses `int` intentionally for portability. +The `XELPREG` typedef can be overridden to `long` if wider registers +are needed. + +### ESP32 / ESP-IDF + +Install as component: `idf.py add-dependency "deftio/xelp"`. xelp +auto-registers via `CMakeLists.txt` when placed in `components/`. + +--- + ## File structure ``` -src/xelp.c -- implementation (~1130 lines) -src/xelp.h -- public API (types, macros, function declarations) -src/xelpcfg.h -- compile-time feature flags and settings +src/xelp.c -- implementation (~1141 lines) +src/xelp.h -- public API (types, macros, function declarations) +src/xelpcfg.h -- compile-time feature flags and settings +src/XelpArduino.h -- C++ wrapper class for Arduino +``` + +Add these files to your build. No other dependencies. + +### Repository layout + +``` +xelp/ + src/ xelp.c, xelp.h, xelpcfg.h, XelpArduino.h + tests/ unit tests (JumpBug framework), fuzz harnesses + examples/ 13 examples across POSIX, Arduino, ESP32, Pico, bare-metal + docs/ API reference, tutorial, configuration, porting, testing guides + pages/ GitHub Pages website (HTML) + tools/ cross-build scripts, release pipeline, banner generator + dev/ design notes, size profiling + .github/ CI workflows (build, test, fuzz, PlatformIO) ``` -Add these three files to your build. No other dependencies. +### Examples directory + +| Directory | Platform | Description | +|-----------|----------|-------------| +| `bare-metal/` | Any MCU | Minimal porting template | +| `multi-instance/` | Any MCU | Two independent CLIs on separate UARTs | +| `posix-simple/` | Linux/macOS | Full interactive CLI with ncurses | +| `posix-simple-cpp/` | Linux/macOS | Same as above with C++ wrapper | +| `scripting/` | Linux/macOS | Script execution vs interactive mode | +| `arduino/` | Arduino | Raw C API with LED control | +| `arduino-cpp/` | Arduino | C++ `XelpCLI` wrapper | +| `arduino-live-cli/` | Arduino | Full hardware CLI: GPIO, ADC, PWM, tone, pulse, pin scan | +| `esp32-wifi/` | ESP32 | WiFi config, time/weather fetch | +| `esp32c6-wifi/` | ESP32-C6 | Dual-instance Serial + BLE with WiFi commands, Web Bluetooth terminal | +| `esp32-ble-cli/` | ESP32 (any with BLE) | Dual-instance Serial + BLE CLI, Web Bluetooth terminal, cross-instance messaging | +| `pico-cli/` | Raspberry Pi Pico | Pure C (GPIO, ADC, PWM) | +| `pico-cli-arduino/` | Raspberry Pi Pico | Arduino-Pico core + C++ wrapper | + +--- + +## Compiled sizes (ARM Thumb -Os, representative) + +| Configuration | .text (bytes) | +|--------------|-------------:| +| KEY only | ~580 | +| CLI only | ~1508 | +| CLI + line edit | ~1952 | +| CLI + line edit + help + key | ~2466 | +| Full (all features) | ~2506 | +| Full + history | ~2922 | + +Full size tables for 20+ architectures in [README.md](README.md). + +--- + +## Quick reference: all public functions + +| Function | Purpose | +|----------|---------| +| `XelpInit(ths, aboutMsg)` | Initialize instance | +| `XelpParseKey(ths, char)` | Feed one interactive character | +| `XelpParse(ths, buf, len)` | Execute script buffer | +| `XelpParseXB(ths, &xb)` | Execute script from XelpBuf | +| `XelpExecKC(ths, keycode)` | Execute single-key command directly | +| `XelpOut(ths, msg, maxlen)` | Output string | +| `XelpPutc(ths, char)` | Output single character | +| `XelpHelp(ths)` | Print help listing | +| `XelpTokLineXB(&buf, &tok, type)` | Tokenize next token or line | +| `XelpTokN(&buf, n, &tok)` | Get Nth token (random access) | +| `XelpNumToks(&buf, &n)` | Count tokens | +| `XelpArgsInit(&a, args, len)` | Init sequential iterator | +| `XelpNextTok(&a, &tok)` | Get next token | +| `XelpNextInt(&a, &val)` | Get next token as int | +| `XelpArgCount(&a, &n)` | Count tokens (non-destructive) | +| `XelpArgInt(args, len, n, &val)` | Get arg N as int (one-shot) | +| `XelpArgStr(args, len, n, &s, &slen)` | Get arg N as string span | +| `XelpStrLen(s)` | String length | +| `XelpStrEq(buf, len, cmd)` | Compare buffer to string | +| `XelpStrEq2(buf, end, cmd)` | Compare pointer-pair to string | +| `XelpStr2Int(s, maxlen)` | Parse string to int | +| `XelpParseNum(s, maxlen, &n)` | Safer parse with result code | +| `XelpBufCmp(as, ae, bs, be, type)` | Compare two buffers | +| `XelpFindTok(&xb, t0s, t0e, type)` | Find matching token | + +--- + +## License + +BSD 2-Clause. See [LICENSE.txt](LICENSE.txt). + +Copyright (c) 2011-2026, M. A. Chatterjee. diff --git a/CHANGELOG.md b/CHANGELOG.md index c588cc3..f38d0c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ Versions always use three-component semver (e.g. `0.3.0`, never `0.3`). ## [Unreleased] +### Added +- **ESP32 BLE CLI example** (`examples/esp32-ble-cli/`): dual-instance xelp + demo with one CLI on USB Serial and one on BLE (Nordic UART Service). + Both instances share the same command table. Includes Web Bluetooth + terminal (`web/index.html`), Makefile, and cross-instance messaging + (`sendmsg` command). Compatible with any BLE-capable ESP32 variant. + Requires NimBLE-Arduino library. +- `rgb` command in esp32-ble-cli for direct RGB NeoPixel color control on + boards like Unexpected Maker ProS3/TinyS3/FeatherS3. +- `sendmsg ` cross-instance messaging command in + esp32-ble-cli, with thread-safe deferred delivery via loop(). +- Drip-feed BLE notification pacing (one 20-byte chunk per connection + interval) for reliable output over Nordic UART Service. +- Deferred serial banner (press Enter to show) to avoid USB CDC boot + truncation on ESP32-S3. + +### Fixed +- `\r` stripping in Web Bluetooth terminals (`esp32-ble-cli` and + `esp32c6-wifi`) to prevent line overwrite in browsers. + +### Changed +- Updated READMEs for arduino, arduino-cpp, esp32-wifi, pico-cli, and + pico-cli-arduino examples with arduino-cli compile/upload/monitor + commands. + ## [0.3.2] - 2026-04-26 ### Added diff --git a/README.md b/README.md index 3fdb9f1..c6b9735 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,7 @@ Docker cross-compilation (`tools/Dockerfile.crossbuild`): xelp/ src/ xelp.c, xelp.h, xelpcfg.h (the library -- add these to your project) tests/ unit tests (jumpbug framework), fuzz harnesses, 100% coverage - examples/ POSIX ncurses, Arduino, C++ wrapper, bare-metal, scripting, ESP32 Wi-Fi + examples/ POSIX ncurses, Arduino, C++ wrapper, bare-metal, scripting, ESP32 Wi-Fi/BLE, Pico tools/ cross-build scripts, release script, banner generator docs/ API reference, configuration guide, porting guide pages/ GitHub Pages site @@ -359,8 +359,8 @@ what we welcome, branch model, and the release process. If you use AI coding agents (Claude Code, Cursor, Copilot, etc.), xelp provides machine-readable context files for accurate code generation: -- [AGENTS.md](AGENTS.md) -- concise coding reference: function signatures, setup patterns, common mistakes -- [llms.txt](llms.txt) -- project overview and documentation index ([llmstxt.org](https://llmstxt.org) format) +- [AGENTS.md](AGENTS.md) -- comprehensive coding reference: architecture, function signatures, parser internals, setup patterns, testing, common mistakes +- [llms.txt](llms.txt) -- project overview and full documentation index ([llmstxt.org](https://llmstxt.org) format) ## License diff --git a/dev/manu_xelp_notes_legacy.md b/dev/manu_xelp_notes_legacy.md index 02672cd..244613a 100644 --- a/dev/manu_xelp_notes_legacy.md +++ b/dev/manu_xelp_notes_legacy.md @@ -312,6 +312,10 @@ Variable packing format: 010 = integer (sizeof int) 011 = float32 in 8.24 format 111 = extended form (future) + +[byte1..name_len] # string name of variable, if applicable +[if string size of var content] # if str or array this is a 2 byte size, other wise its the beginning of the value bytes +[bytes : variable content] # if string or array will be size bytes long ``` ## Namespace hierarchy diff --git a/docs/README.md b/docs/README.md index f3281ff..f0ebd5d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,11 @@ Detailed documentation for the xelp embedded CLI/script interpreter library. - [Porting Guide](porting.md) -- How to integrate xelp on your target platform - [Testing Guide](testing.md) -- JumpBug test framework reference +## AI / LLM Integration + +- [AGENTS.md](../AGENTS.md) -- Comprehensive coding reference for AI agents: function signatures, parser internals, setup patterns, testing, common mistakes +- [llms.txt](../llms.txt) -- Project overview and documentation index ([llmstxt.org](https://llmstxt.org) format) + ## Quick Links - [Source code](https://github.com/deftio/xelp) diff --git a/docs/examples.md b/docs/examples.md index f173a72..e9a319f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -12,7 +12,12 @@ example source code is in the `examples/` directory of the repository. | [Posix simple](#posix-simple) | Linux / macOS | Full interactive demo with ncurses | | [Arduino](#arduino) | Any Arduino board | LED control, token listing via CLI | | [Arduino C++](#arduino-cpp) | Any Arduino board | C++ wrapper class for easy integration | +| [Arduino Live CLI](#arduino-live-cli) | Any Arduino board | Full hardware CLI: GPIO, ADC, PWM, tone, pulse | | [ESP32 WiFi](#esp32-wifi) | ESP32 | WiFi config, time and weather fetch via CLI | +| [ESP32-C6 WiFi + BLE](#esp32c6-wifi) | ESP32-C6 | Dual-instance Serial + BLE with WiFi | +| [ESP32 BLE CLI](#esp32-ble-cli) | Any ESP32 with BLE | Dual-instance Serial + BLE, Web Bluetooth terminal | +| [Pico CLI (C)](#pico-cli) | Raspberry Pi Pico | GPIO, ADC, PWM over USB serial (Pico SDK) | +| [Pico CLI (Arduino)](#pico-cli-arduino) | Raspberry Pi Pico | C++ Easy API with lambdas | | [Scripting](#scripting) | Linux / macOS | Batch scripting vs interactive mode | ## Bare metal @@ -219,6 +224,79 @@ APIs (worldtimeapi.org and open-meteo.com). No API keys needed. - `time` -- fetch current time - `weather ` -- fetch weather for coordinates +## Arduino Live CLI + +**File:** `examples/arduino-live-cli/arduino-live-cli.ino` + +Full interactive hardware CLI using the C++ wrapper. Over 15 commands for +GPIO, ADC, PWM, tone generation, pulse width measurement, and pin scanning. +Works on any Arduino-compatible board (AVR, ARM, ESP32, RP2040). + +### Commands + +- `help` / `?` -- list all commands +- `setpin <0|1>` / `getpin ` -- digital I/O +- `pinmode ` -- configure direction +- `setpwm <0-255>` / `readadc ` -- analog I/O +- `tone ` / `notone ` -- tone generation +- `pulsein ` -- measure pulse width +- `scanpins ` -- scan GPIO states +- `reboot` -- software reset + +## ESP32-C6 WiFi + BLE + +**Files:** `examples/esp32c6-wifi/` + +Dual-instance demo for Seeed XIAO ESP32-C6: one CLI on USB Serial, one on +BLE (Nordic UART Service). WiFi commands for scanning and connecting. +Includes Web Bluetooth terminal in `web/index.html`. + +## ESP32 BLE CLI + +**Files:** `examples/esp32-ble-cli/` + +Dual-instance CLI for any BLE-capable ESP32. One xelp instance on USB +Serial, one on BLE (Nordic UART Service), both sharing the same command +table. Demonstrates zero-dynamic-memory multi-instance CLI over two +different transports with cross-instance messaging. + +### Key features + +- Drip-feed BLE notification pacing (one 20-byte chunk per connection interval) +- Cross-instance messaging (`sendmsg serial|ble `) +- RGB NeoPixel LED support with automatic LDO2 power enable +- Web Bluetooth terminal (`web/index.html`) for Chrome/Edge +- App version tracking in `info` command + +### Commands + +- `help` / `?`, `banner`, `echo`, `info` +- `led <0|1>`, `rgb ` -- LED control +- `setpin`, `getpin`, `pinmode`, `setpwm`, `readadc` -- GPIO/analog +- `status`, `sendmsg `, `reboot` + +## Pico CLI (C) + +**Files:** `examples/pico-cli/` + +Pure C example for Raspberry Pi Pico / Pico W / Pico 2 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. + +```bash +mkdir build && cd build +cmake -DPICO_BOARD=pico .. # or pico_w, pico2, pico2_w +make +``` + +## Pico CLI (Arduino) + +**Files:** `examples/pico-cli-arduino/` + +Showcases the C++ Easy API on Raspberry Pi Pico using the Arduino-Pico core. +Commands registered with `commands({...})` -- no static tables, no raw `XELP*` +pointers. Lambda callbacks receive `XelpCLI&`, `argc`, and `argv`. + ## Scripting **File:** `examples/scripting/scripting-example.c` diff --git a/examples/arduino-cpp/README.md b/examples/arduino-cpp/README.md index e686eb3..f4f2a90 100644 --- a/examples/arduino-cpp/README.md +++ b/examples/arduino-cpp/README.md @@ -12,10 +12,28 @@ C-style static command table. Eliminates boilerplate: no manual ## Setup +### Arduino IDE + 1. Open `arduino-cpp.ino` in the Arduino IDE. 2. Select your board and port. 3. Upload and open the Serial Monitor at **115200 baud**. +### arduino-cli + +```bash +# List connected boards to find your port and FQBN +arduino-cli board list + +# Compile (replace FQBN with your board) +arduino-cli compile --fqbn arduino:avr:uno examples/arduino-cpp + +# Upload +arduino-cli upload --fqbn arduino:avr:uno -p /dev/ttyACM0 examples/arduino-cpp + +# Serial monitor +arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200 +``` + ## Commands | Command | Description | diff --git a/examples/arduino-live-cli/README.md b/examples/arduino-live-cli/README.md index 4dc83c0..32726c1 100644 --- a/examples/arduino-live-cli/README.md +++ b/examples/arduino-live-cli/README.md @@ -5,10 +5,11 @@ and play tones from any serial terminal. ## Requirements -- Any Arduino board with a Serial port (Uno, Nano, Mega 2560, Leonardo, etc.) -- Arduino IDE 1.8+ or Arduino Cloud +- Any Arduino board with a Serial port (Uno, Nano, Mega 2560, Leonardo, + Raspberry Pi Pico / Pico 2, ESP32, etc.) +- Arduino IDE 1.8+, Arduino Cloud, arduino-cli, or PlatformIO -## Quick start +## Quick start (Arduino IDE) 1. Open `arduino-live-cli.ino` in the Arduino IDE 2. Select your board and port @@ -16,6 +17,36 @@ and play tones from any serial terminal. 4. Open Serial Monitor at **115200 baud** 5. Type `help` (or just `?`) +## Quick start (arduino-cli) + +```bash +# Compile (substitute the FQBN for your board): +arduino-cli compile --fqbn rp2040:rp2040:rpipico2 --library path/to/xelp/src . + +# Upload via serial port: +arduino-cli upload --fqbn rp2040:rp2040:rpipico2 -p /dev/ttyACM0 . + +# Or for RP2040/RP2350 boards: copy the UF2 file directly. +# Hold BOOTSEL, plug in USB, then: +cp build/rp2040.rp2040.rpipico2/arduino-live-cli.ino.uf2 /Volumes/RP2350/ +``` + +Common FQBNs: + +| Board | FQBN | +|-------|------| +| Raspberry Pi Pico 2 | `rp2040:rp2040:rpipico2` | +| Raspberry Pi Pico | `rp2040:rp2040:rpipico` | +| Arduino Mega 2560 | `arduino:avr:mega` | +| Arduino Uno | `arduino:avr:uno` | +| ESP32 Dev Module | `esp32:esp32:esp32` | + +## Quick start (PlatformIO) + +Install xelp from the PlatformIO registry (`pio pkg install -l xelp`), +copy `arduino-live-cli.ino` into your project's `src/` directory, and +build normally with `pio run`. + ## Commands | Command | Usage | Description | @@ -30,19 +61,23 @@ and play tones from any serial terminal. | `pinmode` | `pinmode ` | Configure pin direction | | `setpwm` | `setpwm <0-255>` | `analogWrite` -- set PWM duty cycle | | `readadc` | `readadc ` | `analogRead` -- print 0-1023 | -| `tone` | `tone [ms]` | Play a frequency on a buzzer/speaker | -| `notone` | `notone ` | Stop tone output | -| `pulsein` | `pulsein [timeout]` | Measure pulse width (microseconds) | +| `tone` | `tone [ms]` | Play a frequency on a buzzer/speaker * | +| `notone` | `notone ` | Stop tone output * | +| `pulsein` | `pulsein [timeout]` | Measure pulse width (microseconds) * | | `delay` | `delay ` | Pause for N milliseconds | | `millis` | `millis` | Print uptime in milliseconds | -| `micros` | `micros` | Print uptime in microseconds | -| `scanpins` | `scanpins [first] [last]` | Read all digital pins in range | -| `setprompt` | `setprompt ` | Change the CLI prompt string | -| `demo-blink3` | `demo-blink3` | Blink LED 3x -- scripting demo | -| `demo-scan` | `demo-scan` | Configure + scan pins -- scripting demo | -| `demo-info` | `demo-info` | Chain info commands -- scripting demo | +| `micros` | `micros` | Print uptime in microseconds * | +| `scanpins` | `scanpins [first] [last]` | Read all digital pins in range * | +| `demo-blink3` | `demo-blink3` | Blink LED 3x -- scripting demo * | +| `demo-scan` | `demo-scan` | Configure + scan pins -- scripting demo * | +| `demo-info` | `demo-info` | Chain info commands -- scripting demo * | | `reboot` | `reboot` | Software reset (AVR / ESP32) | +Commands marked with **\*** are excluded on ATmega328P/168 boards (see +notes below). + +Unrecognized commands print an error: `foo: unknown command`. + ## Command history Use the up/down arrow keys to recall the last 4 commands. @@ -76,3 +111,6 @@ the box. For the full demo, connect: On other platforms it prints an error. - Free SRAM reporting is AVR-only; other platforms show xelp version and uptime only. +- **ATmega328P / ATmega168** (Uno, Nano): a reduced command set is + compiled automatically (`XELP_SMALL_TARGET`). Commands marked with + **\*** in the table above are excluded to fit within 2 KB SRAM. diff --git a/examples/arduino-live-cli/arduino-live-cli.ino b/examples/arduino-live-cli/arduino-live-cli.ino index 66381c8..9e4cb61 100644 --- a/examples/arduino-live-cli/arduino-live-cli.ino +++ b/examples/arduino-live-cli/arduino-live-cli.ino @@ -15,19 +15,23 @@ * BSD-2-Clause -- see LICENSE.txt */ -#define XELP_MAX_CLI_CMDS 24 #include "xelp.h" -#include "XelpArduino.h" + +/* Boards with very limited SRAM get a reduced command set. */ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) +#define XELP_SMALL_TARGET 1 +#endif /* ------------------------------------------------------------------ */ /* Globals */ /* ------------------------------------------------------------------ */ -XelpCLI cli; - -static char promptBuf[32] = "xelp>"; +XELP cli; -void myOutput(char c) { Serial.write(c); } +void myOutput(char c) { + if (c == '\n') Serial.write('\r'); /* raw terminals need \r\n */ + Serial.write(c); +} /* ------------------------------------------------------------------ */ /* Helpers */ @@ -51,244 +55,371 @@ static int freeMemory() { static int freeMemory() { return -1; } #endif +/* Extract token N as a null-terminated string into buf (returns buf). */ +static char* tokStr(const char* args, int len, int n, char* buf, int bsz) { + XelpBuf b, t; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, n, &t) == XELP_S_OK) { + int tl = (int)(t.p - t.s); + if (tl >= bsz) tl = bsz - 1; + memcpy(buf, t.s, tl); + buf[tl] = '\0'; + } else { + buf[0] = '\0'; + } + return buf; +} + +/* Extract token N as an integer. */ +static int tokInt(const char* args, int len, int n) { + char buf[16]; + tokStr(args, len, n, buf, sizeof(buf)); + return atoi(buf); +} + +/* Count tokens. */ +static int tokCount(const char* args, int len) { + XelpBuf b; + int n; + XELP_XB_INIT(b, (char*)args, len); + XelpNumToks(&b, &n); + return n; +} + /* ------------------------------------------------------------------ */ -/* Demo scripts */ +/* Demo scripts (full targets only) */ /* ------------------------------------------------------------------ */ -static const char DEMO_BLINK[] = +#ifndef XELP_SMALL_TARGET +static const char DEMO_BLINK[] PROGMEM = "pinmode 13 out; setpin 13 1; delay 300; setpin 13 0; delay 300; " "setpin 13 1; delay 300; setpin 13 0; delay 300; " "setpin 13 1; delay 300; setpin 13 0"; -static const char DEMO_SCAN[] = +static const char DEMO_SCAN[] PROGMEM = "pinmode 2 in; pinmode 3 in; pinmode 4 in; scanpins 2 4"; +static const char DEMO_INFO[] PROGMEM = + "echo --- board status ---; info; echo uptime:; millis"; + +/* Copy a PROGMEM string to a stack buffer and run it. */ +static void runProgmem(const char* pgm) { + char buf[128]; + strncpy_P(buf, pgm, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + Serial.println(F("Running:")); + Serial.print(F(" ")); + Serial.println(buf); + Serial.println(); + XelpParse(&cli, buf, (int)strlen(buf)); +} +#endif /* XELP_SMALL_TARGET */ + /* ------------------------------------------------------------------ */ -/* Setup */ +/* Command handlers */ /* ------------------------------------------------------------------ */ -void setup() { - Serial.begin(115200); - while (!Serial) ; /* wait for USB serial (Leonardo) */ - pinMode(LED_BUILTIN, OUTPUT); +XELPRESULT cmdHelp(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, XELP_BANNER_STR, 0); + return XelpHelp(x); +} - cli.begin("arduino-live-cli -- xelp hardware CLI demo\n", &myOutput); - cli.setPrompt(promptBuf); - - cli.commands({ - - /* ---- Help & info ------------------------------------------ */ - {"help", "show this help listing", - [](XelpCLI& c, int, const char**) { - c.help(); - }}, - - {"?", "same as help", - [](XelpCLI& c, int, const char**) { - c.help(); - }}, - - {"banner", "print xelp banner", - [](XelpCLI& c, int, const char**) { - c.print(XELP_BANNER_STR); - c.print("Welcome to xelp CLI demo.\n"); - c.print("Type help to see commands. (also accepts ?)\n"); - }}, - - {"echo", "echo -- print arguments", - [](XelpCLI& c, int argc, const char** argv) { - for (int i = 1; i < argc; i++) { - if (i > 1) c.print(" "); - c.print(argv[i]); - } - c.print("\n"); - }}, - - {"info", "print board type, memory, uptime", - [](XelpCLI& c, int, const char**) { - char buf[64]; +XELPRESULT cmdBanner(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, XELP_BANNER_STR, 0); + XelpOut(x, "Welcome to xelp CLI demo.\n", 0); + XelpOut(x, "Type help to see commands. (also accepts ?)\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdEcho(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + char buf[32]; + for (int i = 1; i < n; i++) { + if (i > 1) XelpOut(x, " ", 1); + tokStr(args, len, i, buf, sizeof(buf)); + XelpOut(x, buf, 0); + } + XelpOut(x, "\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdInfo(XELP *x, const char* a, int l) { + (void)a; (void)l; + char buf[64]; #if defined(ARDUINO_BOARD) - snprintf(buf, sizeof(buf), "Board: %s\n", ARDUINO_BOARD); + snprintf(buf, sizeof(buf), "Board: %s\n", ARDUINO_BOARD); #else - snprintf(buf, sizeof(buf), "Board: unknown\n"); + snprintf(buf, sizeof(buf), "Board: unknown\n"); #endif - c.print(buf); - int mem = freeMemory(); - if (mem >= 0) { - snprintf(buf, sizeof(buf), "Free: %d bytes\n", mem); - c.print(buf); - } - snprintf(buf, sizeof(buf), "Uptime: %lu ms\n", millis()); - c.print(buf); - snprintf(buf, sizeof(buf), "xelp: 0x%08lX\n", - (unsigned long)XELP_VERSION); - c.print(buf); - }}, - - /* ---- Digital I/O ------------------------------------------ */ - {"setpin", "setpin <0|1> -- digitalWrite", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 3) { c.print("usage: setpin <0|1>\n"); return; } - int pin = atoi(argv[1]); - warnSerial(pin); - digitalWrite(pin, atoi(argv[2]) ? HIGH : LOW); - }}, - - {"getpin", "getpin -- digitalRead", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 2) { c.print("usage: getpin \n"); return; } - char buf[8]; - snprintf(buf, sizeof(buf), "%d\n", digitalRead(atoi(argv[1]))); - c.print(buf); - }}, - - {"pinmode", "pinmode ", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 3) { c.print("usage: pinmode \n"); return; } - int pin = atoi(argv[1]); - warnSerial(pin); - if (strcmp(argv[2], "out") == 0) pinMode(pin, OUTPUT); - else if (strcmp(argv[2], "in") == 0) pinMode(pin, INPUT); - else if (strcmp(argv[2], "pullup") == 0) pinMode(pin, INPUT_PULLUP); - else c.print(" expected: in, out, or pullup\n"); - }}, - - /* ---- Analog ----------------------------------------------- */ - {"setpwm", "setpwm <0-255> -- analogWrite", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 3) { c.print("usage: setpwm <0-255>\n"); return; } - int pin = atoi(argv[1]); - warnSerial(pin); - analogWrite(pin, atoi(argv[2])); - }}, - - {"readadc", "readadc -- analogRead (0-1023)", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 2) { c.print("usage: readadc \n"); return; } - char buf[8]; - snprintf(buf, sizeof(buf), "%d\n", analogRead(atoi(argv[1]))); - c.print(buf); - }}, - - /* ---- Tone ------------------------------------------------- */ - {"tone", "tone [ms] -- play frequency", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 3) { c.print("usage: tone [ms]\n"); return; } - int pin = atoi(argv[1]); - unsigned int freq = (unsigned int)atoi(argv[2]); - if (argc >= 4) - tone(pin, freq, (unsigned long)atol(argv[3])); - else - tone(pin, freq); - }}, - - {"notone", "notone -- stop tone", - [](XelpCLI&, int argc, const char** argv) { - if (argc < 2) return; - noTone(atoi(argv[1])); - }}, - - /* ---- Pulse measurement ------------------------------------ */ - {"pulsein", "pulsein [timeout_us]", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 3) { c.print("usage: pulsein [timeout_us]\n"); return; } - int pin = atoi(argv[1]); - int level = (strcmp(argv[2], "high") == 0) ? HIGH : LOW; - unsigned long timeout = (argc >= 4) - ? (unsigned long)atol(argv[3]) : 1000000UL; - unsigned long dur = pulseIn(pin, level, timeout); - char buf[24]; - snprintf(buf, sizeof(buf), "%lu us\n", dur); - c.print(buf); - }}, - - /* ---- Timing ----------------------------------------------- */ - {"delay", "delay -- pause execution", - [](XelpCLI&, int argc, const char** argv) { - if (argc >= 2) delay((unsigned long)atol(argv[1])); - }}, - - {"millis", "print uptime in milliseconds", - [](XelpCLI& c, int, const char**) { - char buf[16]; - snprintf(buf, sizeof(buf), "%lu\n", millis()); - c.print(buf); - }}, - - {"micros", "print uptime in microseconds", - [](XelpCLI& c, int, const char**) { - char buf[16]; - snprintf(buf, sizeof(buf), "%lu\n", micros()); - c.print(buf); - }}, - - /* ---- Pin scan --------------------------------------------- */ - {"scanpins", "scanpins [first] [last] -- read digital pins", - [](XelpCLI& c, int argc, const char** argv) { - int first = (argc >= 2) ? atoi(argv[1]) : 0; - int last = (argc >= 3) ? atoi(argv[2]) : NUM_DIGITAL_PINS - 1; - char buf[24]; - c.print("pin value\n"); - c.print("--- -----\n"); - for (int p = first; p <= last; p++) { - snprintf(buf, sizeof(buf), "%3d %5d\n", p, digitalRead(p)); - c.print(buf); - } - }}, - - /* ---- Prompt ----------------------------------------------- */ - {"setprompt", "setprompt -- change CLI prompt", - [](XelpCLI& c, int argc, const char** argv) { - if (argc < 2) { c.print("usage: setprompt \n"); return; } - strncpy(promptBuf, argv[1], sizeof(promptBuf) - 1); - promptBuf[sizeof(promptBuf) - 1] = '\0'; - c.setPrompt(promptBuf); - }}, - - /* ---- Demos ------------------------------------------------ */ - {"demo-blink3", "blink LED 3x (scripting demo)", - [](XelpCLI& c, int, const char**) { - c.print("xelp can chain commands as scripts using semicolons.\n"); - c.print("Running:\n "); - c.print(DEMO_BLINK); - c.print("\n\n"); - c.run(DEMO_BLINK); - c.print("Done.\n"); - }}, - - {"demo-scan", "configure + scan pins (scripting demo)", - [](XelpCLI& c, int, const char**) { - c.print("Set pins 2-4 as inputs, then scan them:\n "); - c.print(DEMO_SCAN); - c.print("\n\n"); - c.run(DEMO_SCAN); - }}, - - {"demo-info", "echo + info + millis (scripting demo)", - [](XelpCLI& c, int, const char**) { - static const char script[] = - "echo --- board status ---; info; echo uptime:; millis"; - c.print("Chain multiple info commands in one line:\n "); - c.print(script); - c.print("\n\n"); - c.run(script); - }}, - - /* ---- Reboot ----------------------------------------------- */ - {"reboot", "software reset", - [](XelpCLI& c, int, const char**) { - c.print("Rebooting...\n"); - delay(100); + XelpOut(x, buf, 0); + int mem = freeMemory(); + if (mem >= 0) { + snprintf(buf, sizeof(buf), "Free: %d bytes\n", mem); + XelpOut(x, buf, 0); + } + snprintf(buf, sizeof(buf), "Uptime: %lu ms\n", millis()); + XelpOut(x, buf, 0); + snprintf(buf, sizeof(buf), "xelp: 0x%08lX\n", (unsigned long)XELP_VERSION); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdSetpin(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: setpin <0|1>\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + warnSerial(pin); + digitalWrite(pin, tokInt(args, len, 2) ? HIGH : LOW); + return XELP_S_OK; +} + +XELPRESULT cmdGetpin(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 2) { + XelpOut(x, "usage: getpin \n", 0); + return XELP_E_ERR; + } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", digitalRead(tokInt(args, len, 1))); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdPinmode(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: pinmode \n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + char mode[8]; + tokStr(args, len, 2, mode, sizeof(mode)); + warnSerial(pin); + if (strcmp(mode, "out") == 0) pinMode(pin, OUTPUT); + else if (strcmp(mode, "in") == 0) pinMode(pin, INPUT); + else if (strcmp(mode, "pullup") == 0) pinMode(pin, INPUT_PULLUP); + else XelpOut(x, " expected: in, out, or pullup\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdSetpwm(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: setpwm <0-255>\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + warnSerial(pin); + analogWrite(pin, tokInt(args, len, 2)); + return XELP_S_OK; +} + +XELPRESULT cmdReadadc(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 2) { + XelpOut(x, "usage: readadc \n", 0); + return XELP_E_ERR; + } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", analogRead(tokInt(args, len, 1))); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +#ifndef XELP_SMALL_TARGET +XELPRESULT cmdTone(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + if (n < 3) { + XelpOut(x, "usage: tone [ms]\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + unsigned int freq = (unsigned int)tokInt(args, len, 2); + if (n >= 4) { + char buf[16]; + tone(pin, freq, (unsigned long)atol(tokStr(args, len, 3, buf, sizeof(buf)))); + } else { + tone(pin, freq); + } + return XELP_S_OK; +} + +XELPRESULT cmdNotone(XELP *x, const char* args, int len) { + (void)x; + if (tokCount(args, len) < 2) return XELP_E_ERR; + noTone(tokInt(args, len, 1)); + return XELP_S_OK; +} + +XELPRESULT cmdPulsein(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + if (n < 3) { + XelpOut(x, "usage: pulsein [timeout_us]\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + char mode[8]; + tokStr(args, len, 2, mode, sizeof(mode)); + int level = (strcmp(mode, "high") == 0) ? HIGH : LOW; + unsigned long timeout = 1000000UL; + if (n >= 4) { + char buf[16]; + timeout = (unsigned long)atol(tokStr(args, len, 3, buf, sizeof(buf))); + } + unsigned long dur = pulseIn(pin, level, timeout); + char out[24]; + snprintf(out, sizeof(out), "%lu us\n", dur); + XelpOut(x, out, 0); + return XELP_S_OK; +} + +XELPRESULT cmdMicros(XELP *x, const char* args, int len) { + (void)args; (void)len; + char buf[16]; + snprintf(buf, sizeof(buf), "%lu\n", micros()); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdScanpins(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + int first = (n >= 2) ? tokInt(args, len, 1) : 0; + int last = (n >= 3) ? tokInt(args, len, 2) : NUM_DIGITAL_PINS - 1; + char buf[24]; + XelpOut(x, "pin value\n", 0); + XelpOut(x, "--- -----\n", 0); + for (int p = first; p <= last; p++) { + snprintf(buf, sizeof(buf), "%3d %5d\n", p, digitalRead(p)); + XelpOut(x, buf, 0); + } + return XELP_S_OK; +} + +XELPRESULT cmdDemoBlink(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, "xelp can chain commands as scripts using semicolons.\n", 0); + runProgmem(DEMO_BLINK); + XelpOut(x, "Done.\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdDemoScan(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, "Set pins 2-4 as inputs, then scan them:\n", 0); + runProgmem(DEMO_SCAN); + return XELP_S_OK; +} + +XELPRESULT cmdDemoInfo(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, "Chain multiple info commands in one line:\n", 0); + runProgmem(DEMO_INFO); + return XELP_S_OK; +} +#endif /* XELP_SMALL_TARGET */ + +XELPRESULT cmdDelay(XELP *x, const char* args, int len) { + (void)x; + if (tokCount(args, len) >= 2) { + char buf[16]; + delay((unsigned long)atol(tokStr(args, len, 1, buf, sizeof(buf)))); + } + return XELP_S_OK; +} + +XELPRESULT cmdMillis(XELP *x, const char* args, int len) { + (void)args; (void)len; + char buf[16]; + snprintf(buf, sizeof(buf), "%lu\n", millis()); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdReboot(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, "Rebooting...\n", 0); + delay(100); #if defined(__AVR__) - void (*resetFunc)(void) = 0; - resetFunc(); + void (*resetFunc)(void) = 0; + resetFunc(); #elif defined(ESP32) || defined(ESP8266) - ESP.restart(); + ESP.restart(); +#else + XelpOut(x, " not supported on this board\n", 0); +#endif + return XELP_S_OK; +} + +/* Default handler for unrecognized commands. */ +XELPRESULT cmdNotFound(XELP *x, const char* args, int len) { + char buf[16]; + tokStr(args, len, 0, buf, sizeof(buf)); + XelpOut(x, buf, 0); + XelpOut(x, ": unknown command\n", 0); + return XELP_E_CMDNOTFOUND; +} + +/* ------------------------------------------------------------------ */ +/* Command table */ +/* ------------------------------------------------------------------ */ + +#ifdef XELP_SMALL_TARGET +XELPCLIFuncMapEntry commands[] = { + { &cmdHelp, "help", "" }, + { &cmdHelp, "?", "" }, + { &cmdBanner, "banner", "" }, + { &cmdEcho, "echo", "" }, + { &cmdInfo, "info", "" }, + { &cmdSetpin, "setpin", " <0|1>" }, + { &cmdGetpin, "getpin", "" }, + { &cmdPinmode, "pinmode", " " }, + { &cmdSetpwm, "setpwm", " <0-255>" }, + { &cmdReadadc, "readadc", "" }, + { &cmdDelay, "delay", "" }, + { &cmdMillis, "millis", "" }, + { &cmdReboot, "reboot", "" }, + XELP_FUNC_ENTRY_LAST +}; #else - c.print(" not supported on this board\n"); +XELPCLIFuncMapEntry commands[] = { + { &cmdHelp, "help", "show this help listing" }, + { &cmdHelp, "?", "same as help" }, + { &cmdBanner, "banner", "print xelp banner" }, + { &cmdEcho, "echo", "echo -- print arguments" }, + { &cmdInfo, "info", "board type, memory, uptime" }, + { &cmdSetpin, "setpin", "setpin <0|1>" }, + { &cmdGetpin, "getpin", "getpin -- digitalRead" }, + { &cmdPinmode, "pinmode", "pinmode " }, + { &cmdSetpwm, "setpwm", "setpwm <0-255>" }, + { &cmdReadadc, "readadc", "readadc -- analogRead" }, + { &cmdTone, "tone", "tone [ms]" }, + { &cmdNotone, "notone", "notone -- stop tone" }, + { &cmdPulsein, "pulsein", "pulsein [us]" }, + { &cmdDelay, "delay", "delay " }, + { &cmdMillis, "millis", "uptime in milliseconds" }, + { &cmdMicros, "micros", "uptime in microseconds" }, + { &cmdScanpins, "scanpins", "scanpins [first] [last]" }, + { &cmdDemoBlink, "demo-blink3", "blink LED 3x (scripting demo)" }, + { &cmdDemoScan, "demo-scan", "configure+scan (scripting demo)" }, + { &cmdDemoInfo, "demo-info", "echo+info+millis (script demo)" }, + { &cmdReboot, "reboot", "software reset" }, + XELP_FUNC_ENTRY_LAST +}; #endif - }}, - }); + +/* ------------------------------------------------------------------ */ +/* Setup */ +/* ------------------------------------------------------------------ */ + +void setup() { + Serial.begin(115200); + while (!Serial) ; /* wait for USB serial (Leonardo) */ + pinMode(LED_BUILTIN, OUTPUT); + + XelpInit(&cli, "arduino-live-cli -- xelp hardware CLI demo\n"); + XELP_SET_FN_OUT(cli, &myOutput); + XELP_SET_FN_CLI(cli, commands); + XELP_SET_FN_DEF_CLI(cli, &cmdNotFound); Serial.println(F(XELP_BANNER_STR)); Serial.println(F("Welcome to xelp CLI demo.")); @@ -300,5 +431,8 @@ void setup() { /* ------------------------------------------------------------------ */ void loop() { - cli.poll(Serial); + while (Serial.available() > 0) { + char c = (char)Serial.read(); + XelpParseKey(&cli, c); + } } diff --git a/examples/arduino/README.md b/examples/arduino/README.md index d898de0..f242c32 100644 --- a/examples/arduino/README.md +++ b/examples/arduino/README.md @@ -11,11 +11,29 @@ and built-in help. No external library dependencies. ## Setup +### Arduino IDE + 1. Open `arduino.ino` in the Arduino IDE. 2. Select your board and port. 3. Upload and open the Serial Monitor at **115200 baud**. 4. Type `help` and press ENTER. +### arduino-cli + +```bash +# List connected boards to find your port and FQBN +arduino-cli board list + +# Compile (replace FQBN with your board) +arduino-cli compile --fqbn arduino:avr:uno examples/arduino + +# Upload +arduino-cli upload --fqbn arduino:avr:uno -p /dev/ttyACM0 examples/arduino + +# Serial monitor +arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200 +``` + ## Commands | Command | Description | diff --git a/examples/esp32-ble-cli/Makefile b/examples/esp32-ble-cli/Makefile new file mode 100644 index 0000000..0a161e6 --- /dev/null +++ b/examples/esp32-ble-cli/Makefile @@ -0,0 +1,74 @@ +# Makefile for esp32-ble-cli xelp example +# +# Wraps arduino-cli for compile, upload, and monitor. +# Auto-detects the first ESP32 USB serial port. +# +# Usage: +# make # compile, upload, and open serial monitor +# make compile # compile only +# make upload # compile and upload +# make monitor # open serial monitor +# make web # serve Web Bluetooth terminal on localhost:8086 +# make clean # remove build artifacts +# make ports # list connected boards +# +# Override defaults: +# make upload FQBN=esp32:esp32:esp32s3 PORT=/dev/ttyUSB0 + +FQBN ?= esp32:esp32:um_pros3 +BAUD ?= 115200 +WEB_PORT ?= 8086 +SKETCH = . + +# Auto-detect first ESP32 USB port (override with PORT=...) +PORT ?= $(shell arduino-cli board list 2>/dev/null \ + | grep -i 'usb.*esp32\|usbmodem' \ + | head -1 | awk '{print $$1}') + +.PHONY: all compile upload monitor web clean ports help + +all: help + +compile: + arduino-cli compile --fqbn $(FQBN) $(SKETCH) + +upload: compile + @if [ -z "$(PORT)" ]; then \ + echo "ERROR: no ESP32 port detected. Plug in the board or set PORT="; \ + exit 1; \ + fi + arduino-cli upload --fqbn $(FQBN) -p $(PORT) $(SKETCH) + +monitor: + @if [ -z "$(PORT)" ]; then \ + echo "ERROR: no ESP32 port detected. Plug in the board or set PORT="; \ + exit 1; \ + fi + arduino-cli monitor -p $(PORT) -c baudrate=$(BAUD) + +web: + @echo "Serving Web Bluetooth terminal at http://localhost:$(WEB_PORT)" + @echo "Open in Chrome or Edge. Press Ctrl-C to stop." + cd web && python3 -m http.server $(WEB_PORT) + +clean: + rm -rf build + +ports: + arduino-cli board list + +help: + @echo "Targets:" + @echo " make compile, upload, open serial monitor" + @echo " make compile compile only" + @echo " make upload compile and upload" + @echo " make monitor serial monitor ($(BAUD) baud)" + @echo " make web serve BLE web terminal on port $(WEB_PORT)" + @echo " make clean remove build artifacts" + @echo " make ports list connected boards" + @echo "" + @echo "Variables:" + @echo " FQBN=$(FQBN)" + @echo " PORT=$(PORT)" + @echo " BAUD=$(BAUD)" + @echo " WEB_PORT=$(WEB_PORT)" diff --git a/examples/esp32-ble-cli/README.md b/examples/esp32-ble-cli/README.md new file mode 100644 index 0000000..0b44108 --- /dev/null +++ b/examples/esp32-ble-cli/README.md @@ -0,0 +1,140 @@ +# ESP32 BLE CLI Example + +Two independent xelp CLI instances on any BLE-capable ESP32: one on USB +Serial, one on BLE (Nordic UART Service). Both share the same command +table and can drive GPIO pins, read ADC, and control the board -- all +with zero dynamic memory allocation. + +## Hardware + +- Any ESP32 with BLE: ESP32, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2 +- Built-in LED (`LED_BUILTIN`, falls back to GPIO 2). Boards with RGB + NeoPixel LEDs (e.g. ProS3, TinyS3, FeatherS3) are auto-detected and use + `rgbLedWrite()` with LDO2 power enable. +- Optional: external LED, potentiometer, or sensor on any GPIO + +## Dependencies + +| Dependency | Install | +|------------|---------| +| ESP32 Arduino core | Arduino IDE Board Manager: esp32 by Espressif | +| NimBLE-Arduino | Arduino IDE Library Manager: search "NimBLE-Arduino" by h2zero | + +## Partition Scheme + +**BLE may exceed the default 1.3 MB app partition on some ESP32 variants.** +If the build fails with "text section exceeds available space", change +the partition scheme: + +> **Tools > Partition Scheme > "Huge APP (3MB No OTA/1MB SPIFFS)"** + +This is an ESP-IDF / Arduino core limitation, not xelp. xelp itself +adds ~4-5 KB. The NimBLE stack accounts for the rest. + +## Files + +``` +esp32-ble-cli/ + esp32-ble-cli.ino Arduino sketch (main source) + xelp.c symlink -> ../../src/xelp.c + xelp.h symlink -> ../../src/xelp.h + xelpcfg.h symlink -> ../../src/xelpcfg.h + XelpArduino.h symlink -> ../../src/XelpArduino.h + web/ + index.html Web Bluetooth terminal (open in Chrome/Edge) +``` + +The symlinks let you develop against the repo's `src/` directly. Arduino +IDE compiles all `.c` files in the sketch folder automatically. + +## Quick Start + +### Arduino IDE + +1. Open `esp32-ble-cli.ino` +2. Select your ESP32 board under Tools > Board +3. If needed, set Tools > Partition Scheme > "Huge APP (3MB No OTA/1MB SPIFFS)" +4. Install NimBLE-Arduino from Library Manager +5. Upload and open Serial Monitor at 115200 baud + +### arduino-cli + +```bash +arduino-cli compile --fqbn esp32:esp32:esp32s3 esp32-ble-cli +arduino-cli upload --fqbn esp32:esp32:esp32s3 -p /dev/ttyUSB0 esp32-ble-cli +arduino-cli monitor -p /dev/ttyUSB0 -c baudrate=115200 +``` + +### PlatformIO + +```ini +[env:esp32] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +lib_deps = h2zero/NimBLE-Arduino +monitor_speed = 115200 +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `help` / `?` | List all commands | +| `banner` | Print xelp banner | +| `echo ` | Print arguments | +| `info` | App version, board type, uptime, xelp version | +| `led <0\|1>` | Built-in LED on/off (RGB NeoPixel or GPIO) | +| `rgb ` | Set RGB LED color (0-255 each, boards with NeoPixel) | +| `setpin <0\|1>` | digitalWrite | +| `getpin ` | digitalRead | +| `pinmode ` | Set pin mode | +| `setpwm <0-255>` | analogWrite (PWM) | +| `readadc ` | analogRead | +| `delay ` | Blocking delay | +| `millis` | Uptime in milliseconds | +| `status` | Serial, BLE, LED type, uptime | +| `sendmsg ` | Send message to the other CLI instance | +| `reboot` | Software reset (ESP.restart) | + +Unknown commands print an error message. + +## Web Bluetooth Terminal + +Open `web/index.html` in Chrome or Edge (Web Bluetooth is not supported +in Firefox or Safari). + +1. Click **Connect** +2. Select **xelp-ble** from the pairing dialog +3. Type commands in the input field or use the quick buttons + +The page is fully self-contained -- no build step, no server required. + +## Serial Usage + +``` +$ screen /dev/tty.usbmodem* 115200 + +xelp> help +... +xelp> setpin 2 1 +xelp> getpin 2 +1 +xelp> readadc 34 +1823 +xelp> status +Serial: connected +BLE: advertising +LED: RGB (NeoPixel) +Uptime: 12345 ms +``` + +## Scripting + +Commands can be chained with semicolons: + +``` +xelp> pinmode 2 out; setpin 2 1; delay 500; setpin 2 0 +``` + +This works identically over both Serial and BLE. diff --git a/examples/esp32-ble-cli/XelpArduino.h b/examples/esp32-ble-cli/XelpArduino.h new file mode 120000 index 0000000..818c614 --- /dev/null +++ b/examples/esp32-ble-cli/XelpArduino.h @@ -0,0 +1 @@ +../../src/XelpArduino.h \ No newline at end of file diff --git a/examples/esp32-ble-cli/esp32-ble-cli.ino b/examples/esp32-ble-cli/esp32-ble-cli.ino new file mode 100644 index 0000000..9b53dd1 --- /dev/null +++ b/examples/esp32-ble-cli/esp32-ble-cli.ino @@ -0,0 +1,589 @@ +/* + * esp32-ble-cli.ino -- ESP32 dual-CLI demo: Serial + BLE. + * + * Two independent xelp instances sharing one command table: one on USB + * Serial, one on BLE (Nordic UART Service). Demonstrates zero-dynamic- + * memory multi-instance CLI over two different transports. + * + * Compatible with any ESP32 variant that has BLE: + * ESP32, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2 + * + * Commands (identical on both Serial and BLE): + * help / ? list commands + * banner print welcome banner + * echo print arguments + * info app version, board type, uptime, xelp version + * led <0|1> built-in LED on/off + * rgb set RGB LED color (boards with NeoPixel) + * setpin setpin <0|1> + * getpin getpin -- digitalRead + * pinmode pinmode + * setpwm setpwm <0-255> + * readadc readadc -- analogRead + * delay delay + * millis uptime in milliseconds + * status serial, BLE, LED type, uptime + * sendmsg sendmsg -- cross-instance msg + * reboot software reset (ESP.restart) + * + * Open Serial Monitor at 115200 baud, or connect from the companion + * web app (examples/esp32-ble-cli/web/index.html) over Web Bluetooth. + * + * Requires: + * - ESP32 Arduino core (Board Manager: esp32 by Espressif) + * - NimBLE-Arduino (Library Manager: search "NimBLE-Arduino" by h2zero) + * + * IMPORTANT -- Partition scheme: + * BLE may exceed the default 1.3 MB partition on some ESP32 variants. + * In Arduino IDE select Tools > Partition Scheme > + * "Huge APP (3MB No OTA/1MB SPIFFS)". + * + * Copyright (C) 2011-2026 M. A. Chatterjee + * BSD-2-Clause -- see LICENSE.txt + */ + +#include + +#include "xelp.h" +#include "XelpArduino.h" + +#define APP_VERSION "0.1.0" + +/* ------------------------------------------------------------------ */ +/* Board defaults */ +/* ------------------------------------------------------------------ */ + +/* Most ESP32 boards define LED_BUILTIN for a simple GPIO LED. + Some (e.g. Unexpected Maker ProS3) use an addressable RGB NeoPixel + via RGB_BUILTIN + rgbLedWrite() instead. We support both. + On ProS3: RGB data = GPIO 18, RGB power (LDO2) = GPIO 17. + RGB_PWR must be driven HIGH before rgbLedWrite() will produce output. */ +#ifdef RGB_BUILTIN +#define HAS_RGB_LED 1 +#else +#define HAS_RGB_LED 0 +#ifndef LED_BUILTIN +#define LED_BUILTIN 2 +#endif +#endif + +/* ------------------------------------------------------------------ */ +/* Nordic UART Service UUIDs */ +/* ------------------------------------------------------------------ */ + +#define NUS_SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" +#define NUS_RX_UUID "6e400002-b5a3-f393-e0a9-e50e24dcca9e" +#define NUS_TX_UUID "6e400003-b5a3-f393-e0a9-e50e24dcca9e" + +/* ------------------------------------------------------------------ */ +/* Globals */ +/* ------------------------------------------------------------------ */ + +static XelpCLI cliSerial; +static XelpCLI cliBle; + +static NimBLEServer* pServer = nullptr; +static NimBLECharacteristic* pTxChar = nullptr; +static bool bleConnected = false; +static uint16_t bleConnId = 0; +static bool bleSendBanner = false; +static bool serialBannerSent = false; + +/* Cross-instance message queue (sendmsg). Messages are buffered here and + delivered from loop() to avoid threading issues when the BLE callback + task and the Arduino loop task both write to Serial simultaneously. */ +static char crossMsg[128]; +static int crossMsgLen = 0; +static XELP* crossMsgDest = nullptr; + +/* ------------------------------------------------------------------ */ +/* Output functions */ +/* ------------------------------------------------------------------ */ + +static void serialOut(char c) { + if (c == '\n') Serial.write('\r'); /* raw terminals need \r\n */ + Serial.write(c); +} + +/* Buffered BLE output -- accumulate into a buffer, then drip-feed one + small notification per loop() iteration. This avoids overwhelming the + BLE TX queue (which is only ~5 deep in NimBLE). One chunk per connection + event (~15-30ms) ensures nothing is silently dropped. */ + +static uint8_t bleTxBuf[1536]; +static int bleTxPos = 0; + +/* Send at most one chunk (up to 20 bytes) from the front of bleTxBuf. + Returns true if there's more data remaining. */ +static bool bleTxDrip() { + if (bleTxPos <= 0 || !bleConnected || !pTxChar) return false; + + int chunk = bleTxPos; + if (chunk > 20) chunk = 20; + + pTxChar->setValue(bleTxBuf, chunk); + if (pTxChar->notify()) { + /* shift remaining data to front */ + bleTxPos -= chunk; + if (bleTxPos > 0) + memmove(bleTxBuf, bleTxBuf + chunk, bleTxPos); + } + /* else: TX queue full, will retry next loop iteration */ + return (bleTxPos > 0); +} + +/* Flush entire buffer (used for banner on connect where we want it all + sent before returning). Paces at 1 chunk per 15ms. */ +static void bleTxFlush() { + while (bleTxPos > 0 && bleConnected) { + bleTxDrip(); + if (bleTxPos > 0) delay(15); + } +} + +static void bleOut(char c) { + if (!bleConnected) return; + bleTxBuf[bleTxPos++] = (uint8_t)c; + if (bleTxPos >= (int)sizeof(bleTxBuf)) + bleTxFlush(); +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/* Extract token N as a null-terminated string into buf (returns buf). */ +static char* tokStr(const char* args, int len, int n, char* buf, int bsz) { + XelpBuf b, t; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, n, &t) == XELP_S_OK) { + int tl = (int)(t.p - t.s); + if (tl >= bsz) tl = bsz - 1; + memcpy(buf, t.s, tl); + buf[tl] = '\0'; + } else { + buf[0] = '\0'; + } + return buf; +} + +/* Extract token N as an integer. */ +static int tokInt(const char* args, int len, int n) { + char buf[16]; + tokStr(args, len, n, buf, sizeof(buf)); + return atoi(buf); +} + +/* Count tokens. */ +static int tokCount(const char* args, int len) { + XelpBuf b; + int n; + XELP_XB_INIT(b, (char*)args, len); + XelpNumToks(&b, &n); + return n; +} + +/* ------------------------------------------------------------------ */ +/* Command handlers */ +/* ------------------------------------------------------------------ */ + +XELPRESULT cmdHelp(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, XELP_BANNER_STR, 0); + return XelpHelp(x); +} + +XELPRESULT cmdBanner(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, XELP_BANNER_STR, 0); + XelpOut(x, "ESP32 BLE CLI demo.\n", 0); + XelpOut(x, "Type help to see commands. (also accepts ?)\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdEcho(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + char buf[32]; + for (int i = 1; i < n; i++) { + if (i > 1) XelpOut(x, " ", 1); + tokStr(args, len, i, buf, sizeof(buf)); + XelpOut(x, buf, 0); + } + XelpOut(x, "\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdInfo(XELP *x, const char* a, int l) { + (void)a; (void)l; + char buf[64]; + snprintf(buf, sizeof(buf), "App: esp32-ble-cli v%s\n", APP_VERSION); + XelpOut(x, buf, 0); +#if defined(ARDUINO_BOARD) + snprintf(buf, sizeof(buf), "Board: %s\n", ARDUINO_BOARD); +#else + snprintf(buf, sizeof(buf), "Board: unknown\n"); +#endif + XelpOut(x, buf, 0); + snprintf(buf, sizeof(buf), "Uptime: %lu ms\n", millis()); + XelpOut(x, buf, 0); + snprintf(buf, sizeof(buf), "xelp: 0x%08lX\n", (unsigned long)XELP_VERSION); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdSetpin(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: setpin <0|1>\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + digitalWrite(pin, tokInt(args, len, 2) ? HIGH : LOW); + return XELP_S_OK; +} + +XELPRESULT cmdGetpin(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 2) { + XelpOut(x, "usage: getpin \n", 0); + return XELP_E_ERR; + } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", digitalRead(tokInt(args, len, 1))); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdPinmode(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: pinmode \n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + char mode[8]; + tokStr(args, len, 2, mode, sizeof(mode)); + if (strcmp(mode, "out") == 0) pinMode(pin, OUTPUT); + else if (strcmp(mode, "in") == 0) pinMode(pin, INPUT); + else if (strcmp(mode, "pullup") == 0) pinMode(pin, INPUT_PULLUP); + else XelpOut(x, " expected: in, out, or pullup\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdSetpwm(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 3) { + XelpOut(x, "usage: setpwm <0-255>\n", 0); + return XELP_E_ERR; + } + int pin = tokInt(args, len, 1); + analogWrite(pin, tokInt(args, len, 2)); + return XELP_S_OK; +} + +XELPRESULT cmdReadadc(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 2) { + XelpOut(x, "usage: readadc \n", 0); + return XELP_E_ERR; + } + char buf[8]; + snprintf(buf, sizeof(buf), "%d\n", analogRead(tokInt(args, len, 1))); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdDelay(XELP *x, const char* args, int len) { + (void)x; + if (tokCount(args, len) >= 2) { + char buf[16]; + delay((unsigned long)atol(tokStr(args, len, 1, buf, sizeof(buf)))); + } + return XELP_S_OK; +} + +XELPRESULT cmdMillis(XELP *x, const char* args, int len) { + (void)args; (void)len; + char buf[16]; + snprintf(buf, sizeof(buf), "%lu\n", millis()); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +XELPRESULT cmdLed(XELP *x, const char* args, int len) { + if (tokCount(args, len) < 2) { + XelpOut(x, "usage: led <0|1>\n", 0); + return XELP_E_ERR; + } + int val = tokInt(args, len, 1); +#if HAS_RGB_LED + rgbLedWrite(RGB_BUILTIN, val ? 32 : 0, val ? 32 : 0, val ? 32 : 0); +#else + digitalWrite(LED_BUILTIN, val ? HIGH : LOW); +#endif + XelpOut(x, val ? "LED ON\n" : "LED OFF\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdRgb(XELP *x, const char* args, int len) { +#if HAS_RGB_LED + int n = tokCount(args, len); + if (n < 4) { + XelpOut(x, "usage: rgb (0-255 each)\n", 0); + return XELP_E_ERR; + } + int r = tokInt(args, len, 1); + int g = tokInt(args, len, 2); + int b = tokInt(args, len, 3); + rgbLedWrite(RGB_BUILTIN, r, g, b); + char buf[32]; + snprintf(buf, sizeof(buf), "RGB(%d,%d,%d)\n", r, g, b); + XelpOut(x, buf, 0); +#else + (void)args; (void)len; + XelpOut(x, "no RGB LED on this board\n", 0); +#endif + return XELP_S_OK; +} + +XELPRESULT cmdStatus(XELP *x, const char* args, int len) { + (void)args; (void)len; + char buf[64]; + XelpOut(x, "Serial: connected\n", 0); + XelpOut(x, bleConnected ? "BLE: connected\n" : "BLE: advertising\n", 0); +#if HAS_RGB_LED + XelpOut(x, "LED: RGB (NeoPixel)\n", 0); +#else + snprintf(buf, sizeof(buf), "LED: GPIO %d\n", LED_BUILTIN); + XelpOut(x, buf, 0); +#endif + snprintf(buf, sizeof(buf), "Uptime: %lu ms\n", millis()); + XelpOut(x, buf, 0); + return XELP_S_OK; +} + +/* Send a message to the other xelp instance. + Usage: sendmsg serial|ble + All tokens after the target are concatenated and queued for delivery + in loop(). This avoids threading issues when BLE callbacks and the + Arduino loop task both write to Serial simultaneously. */ +XELPRESULT cmdSendmsg(XELP *x, const char* args, int len) { + int n = tokCount(args, len); + if (n < 3) { + XelpOut(x, "usage: sendmsg \n", 0); + return XELP_E_ERR; + } + + /* Determine target instance */ + char target[8]; + tokStr(args, len, 1, target, sizeof(target)); + XELP *dest = nullptr; + if (strcmp(target, "serial") == 0 || strcmp(target, "ser") == 0) + dest = cliSerial.raw(); + else if (strcmp(target, "ble") == 0) + dest = cliBle.raw(); + else { + XelpOut(x, " target must be 'serial' or 'ble'\n", 0); + return XELP_E_ERR; + } + + /* Build message into crossMsg buffer for loop() to deliver */ + int pos = 0; + pos += snprintf(crossMsg + pos, sizeof(crossMsg) - pos, "[msg] "); + char buf[32]; + for (int i = 2; i < n; i++) { + if (i > 2 && pos < (int)sizeof(crossMsg) - 1) + crossMsg[pos++] = ' '; + tokStr(args, len, i, buf, sizeof(buf)); + pos += snprintf(crossMsg + pos, sizeof(crossMsg) - pos, "%s", buf); + } + if (pos < (int)sizeof(crossMsg) - 1) + crossMsg[pos++] = '\n'; + crossMsg[pos] = '\0'; + crossMsgLen = pos; + crossMsgDest = dest; + + XelpOut(x, "sent\n", 0); + return XELP_S_OK; +} + +XELPRESULT cmdReboot(XELP *x, const char* a, int l) { + (void)a; (void)l; + XelpOut(x, "Rebooting...\n", 0); + delay(100); + ESP.restart(); + return XELP_S_OK; +} + +/* Default handler for unrecognized commands. */ +XELPRESULT cmdNotFound(XELP *x, const char* args, int len) { + char buf[16]; + tokStr(args, len, 0, buf, sizeof(buf)); + XelpOut(x, buf, 0); + XelpOut(x, ": unknown command\n", 0); + return XELP_E_CMDNOTFOUND; +} + +/* ------------------------------------------------------------------ */ +/* Command table (shared by both instances) */ +/* ------------------------------------------------------------------ */ + +XELPCLIFuncMapEntry commands[] = { + { &cmdHelp, "help", "show this help listing" }, + { &cmdHelp, "?", "same as help" }, + { &cmdBanner, "banner", "print xelp banner" }, + { &cmdEcho, "echo", "echo -- print arguments" }, + { &cmdInfo, "info", "app version, board, uptime" }, + { &cmdLed, "led", "led <0|1> -- built-in LED" }, + { &cmdRgb, "rgb", "rgb -- set RGB LED" }, + { &cmdSetpin, "setpin", "setpin <0|1>" }, + { &cmdGetpin, "getpin", "getpin -- digitalRead" }, + { &cmdPinmode, "pinmode", "pinmode " }, + { &cmdSetpwm, "setpwm", "setpwm <0-255>" }, + { &cmdReadadc, "readadc", "readadc -- analogRead" }, + { &cmdDelay, "delay", "delay " }, + { &cmdMillis, "millis", "uptime in milliseconds" }, + { &cmdStatus, "status", "serial/BLE/LED/uptime status" }, + { &cmdSendmsg, "sendmsg", "sendmsg " }, + { &cmdReboot, "reboot", "software reset" }, + XELP_FUNC_ENTRY_LAST +}; + +/* ------------------------------------------------------------------ */ +/* BLE callbacks */ +/* ------------------------------------------------------------------ */ + +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer *s, NimBLEConnInfo &connInfo) override { + (void)s; + bleConnected = true; + bleConnId = connInfo.getConnHandle(); + bleSendBanner = true; + Serial.println("[BLE] client connected"); + } + void onDisconnect(NimBLEServer *s, NimBLEConnInfo &connInfo, int reason) override { + (void)s; (void)connInfo; (void)reason; + bleConnected = false; + bleTxPos = 0; + Serial.println("[BLE] client disconnected"); + NimBLEDevice::startAdvertising(); + } +}; + +class RxCallbacks : public NimBLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic *c, NimBLEConnInfo &connInfo) override { + (void)connInfo; + std::string val = c->getValue(); + for (size_t i = 0; i < val.length(); i++) { + XelpParseKey(cliBle.raw(), val[i]); + } + /* Don't flush here — notify() from within the NimBLE callback task + is unreliable and causes dropped notifications. loop() handles it. */ + } +}; + +/* ------------------------------------------------------------------ */ +/* BLE setup */ +/* ------------------------------------------------------------------ */ + +static void bleSetup(void) +{ + NimBLEDevice::init("xelp-ble"); + pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks()); + + NimBLEService *pService = pServer->createService(NUS_SERVICE_UUID); + + /* TX characteristic: xelp output -> phone (notify) */ + pTxChar = pService->createCharacteristic( + NUS_TX_UUID, NIMBLE_PROPERTY::NOTIFY); + + /* RX characteristic: phone -> xelp input (write) */ + NimBLECharacteristic *pRxChar = pService->createCharacteristic( + NUS_RX_UUID, NIMBLE_PROPERTY::WRITE); + pRxChar->setCallbacks(new RxCallbacks()); + + pService->start(); + + NimBLEAdvertising *pAdv = NimBLEDevice::getAdvertising(); + pAdv->addServiceUUID(NUS_SERVICE_UUID); + NimBLEDevice::startAdvertising(); + + Serial.println("[BLE] advertising as \"xelp-ble\""); +} + +/* ------------------------------------------------------------------ */ +/* Setup */ +/* ------------------------------------------------------------------ */ + +void setup() +{ + Serial.begin(115200); + while (!Serial && millis() < 3000) ; /* wait for USB CDC */ + delay(500); /* let host terminal attach before printing banner */ + +#if HAS_RGB_LED + /* On boards like ProS3/TinyS3/FeatherS3, the NeoPixel is powered via + LDO2 (GPIO 17). Must enable power before rgbLedWrite() works. + RGB_PWR is declared as static const in pins_arduino.h (not a macro), + so we reference the variable directly. */ + pinMode(RGB_PWR, OUTPUT); + digitalWrite(RGB_PWR, HIGH); + delay(10); /* let LDO stabilize */ +#else + pinMode(LED_BUILTIN, OUTPUT); +#endif + + /* Serial CLI */ + cliSerial.begin("ESP32 BLE CLI -- xelp dual-instance demo\n", &serialOut); + cliSerial.setCommands(commands); + cliSerial.setDefaultCommandHandler(&cmdNotFound); + + /* BLE CLI */ + cliBle.begin("ESP32 BLE CLI (BLE)\n", &bleOut); + cliBle.setCommands(commands); + cliBle.setDefaultCommandHandler(&cmdNotFound); + + /* Start BLE */ + bleSetup(); + + /* Defer serial banner to first keypress -- USB CDC on ESP32-S3 may not + have a host terminal attached yet, so printing at boot gets lost. */ + Serial.println("\n[press Enter for CLI]"); +} + +/* ------------------------------------------------------------------ */ +/* Loop */ +/* ------------------------------------------------------------------ */ + +void loop() +{ + /* Show banner on first serial input (avoids USB CDC boot truncation). */ + if (!serialBannerSent && Serial.available()) { + serialBannerSent = true; + cliSerial.run("help"); + cliSerial.parseKey('\r'); + } + + cliSerial.poll(Serial); + + /* Deliver cross-instance messages (from sendmsg) safely from loop(). */ + if (crossMsgDest && crossMsgLen > 0) { + XelpOut(crossMsgDest, crossMsg, crossMsgLen); + crossMsgDest = nullptr; + crossMsgLen = 0; + } + + /* Send banner after BLE connect -- deferred to loop() so the client + has time to subscribe to TX notifications first. */ + if (bleSendBanner && bleConnected) { + bleSendBanner = false; + delay(500); /* let client finish GATT setup + subscribe */ + cliBle.run("help"); + cliBle.parseKey('\r'); /* show prompt */ + bleTxFlush(); + } + + /* Drip-feed BLE output: send one 20-byte chunk per loop iteration. + This paces notifications at ~1 per connection interval so the BLE + stack never overflows its TX queue. */ + if (bleTxPos > 0) { + bleTxDrip(); + delay(15); /* ~1 connection interval */ + } +} diff --git a/examples/esp32-ble-cli/xelp.c b/examples/esp32-ble-cli/xelp.c new file mode 120000 index 0000000..32c4f11 --- /dev/null +++ b/examples/esp32-ble-cli/xelp.c @@ -0,0 +1 @@ +../../src/xelp.c \ No newline at end of file diff --git a/examples/esp32-ble-cli/xelp.h b/examples/esp32-ble-cli/xelp.h new file mode 120000 index 0000000..e51dbf7 --- /dev/null +++ b/examples/esp32-ble-cli/xelp.h @@ -0,0 +1 @@ +../../src/xelp.h \ No newline at end of file diff --git a/examples/esp32-ble-cli/xelpcfg.h b/examples/esp32-ble-cli/xelpcfg.h new file mode 120000 index 0000000..7163600 --- /dev/null +++ b/examples/esp32-ble-cli/xelpcfg.h @@ -0,0 +1 @@ +../../src/xelpcfg.h \ No newline at end of file diff --git a/examples/esp32-wifi/README.md b/examples/esp32-wifi/README.md index c93154c..0b16edb 100644 --- a/examples/esp32-wifi/README.md +++ b/examples/esp32-wifi/README.md @@ -13,10 +13,28 @@ keys needed. ## Setup +### Arduino IDE + 1. Open `esp32-wifi.ino` in the Arduino IDE. 2. Select your ESP32 board and port. 3. Upload and open the Serial Monitor at **115200 baud**. +### arduino-cli + +```bash +# List connected boards to find your port and FQBN +arduino-cli board list + +# Compile (replace FQBN with your board) +arduino-cli compile --fqbn esp32:esp32:esp32s3 examples/esp32-wifi + +# Upload +arduino-cli upload --fqbn esp32:esp32:esp32s3 -p /dev/ttyUSB0 examples/esp32-wifi + +# Serial monitor +arduino-cli monitor -p /dev/ttyUSB0 -c baudrate=115200 +``` + ## Commands | Command | Description | diff --git a/examples/esp32c6-wifi/web/index.html b/examples/esp32c6-wifi/web/index.html index 066c30a..6b5cde7 100644 --- a/examples/esp32c6-wifi/web/index.html +++ b/examples/esp32c6-wifi/web/index.html @@ -134,9 +134,11 @@

xelp BLE Terminal

var val = e.target.value; var text = ''; for (var i = 0; i < val.byteLength; i++) { - text += String.fromCharCode(val.getUint8(i)); + var ch = val.getUint8(i); + if (ch !== 0x0D) /* strip \r */ + text += String.fromCharCode(ch); } - termWrite(text); + if (text) termWrite(text); }); setConnected(true); diff --git a/examples/pico-cli-arduino/README.md b/examples/pico-cli-arduino/README.md index 307c686..16f6a5b 100644 --- a/examples/pico-cli-arduino/README.md +++ b/examples/pico-cli-arduino/README.md @@ -21,8 +21,26 @@ ln -s ../../src/xelpcfg.h . ln -s ../../src/XelpArduino.h . ``` +### Arduino IDE + Open `pico-cli-arduino.ino` in the Arduino IDE and upload. +### arduino-cli + +```bash +# List connected boards to find your port and FQBN +arduino-cli board list + +# Compile (replace FQBN with your board) +arduino-cli compile --fqbn rp2040:rp2040:rpipico examples/pico-cli-arduino + +# Upload +arduino-cli upload --fqbn rp2040:rp2040:rpipico -p /dev/ttyACM0 examples/pico-cli-arduino + +# Serial monitor +arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200 +``` + ## Commands | Command | Description | diff --git a/examples/pico-cli/README.md b/examples/pico-cli/README.md index 07d5c56..42c2cd5 100644 --- a/examples/pico-cli/README.md +++ b/examples/pico-cli/README.md @@ -33,9 +33,14 @@ cmake -DPICO_BOARD=pico .. # or pico_w, pico2, pico2_w make # Flash xelp_pico_cli.uf2 via USB bootloader -``` +# Hold BOOTSEL, plug in USB, copy .uf2 to the mounted drive: +cp xelp_pico_cli.uf2 /Volumes/RP2350/ # macOS +# cp xelp_pico_cli.uf2 /media/$USER/RP2350/ # Linux -Open a serial terminal at 115200 baud after flashing. +# Serial monitor +screen /dev/tty.usbmodem* 115200 # macOS +# screen /dev/ttyACM0 115200 # Linux +``` ## Pico W LED diff --git a/llms.txt b/llms.txt index af490e9..c24a617 100644 --- a/llms.txt +++ b/llms.txt @@ -1,41 +1,149 @@ # xelp -> xelp is a command line interpreter and script engine for embedded systems, -> written in pure C. Zero dynamic memory. No OS required. No standard library -> dependencies. Under 1 KB (key dispatch only) to ~5 KB (full CLI with line -> editing) compiled depending on features enabled. -> Works on 8-bit (AVR, 8051) through 64-bit (ARM64, x86-64) targets. +> xelp is a command line interpreter, script engine, and single-key menu +> system for embedded systems. Pure C, zero dynamic memory, no OS, no +> standard library dependencies. Under 1 KB (key dispatch only) to ~5 KB +> (full CLI with line editing) compiled. Works on 8-bit (AVR, 8051) +> through 64-bit (ARM64, x86-64) targets. Version 0.3.2. xelp gives bare-metal firmware a real interactive CLI with scriptable commands, single-key menus, and pass-through mode. Three source files -(`xelp.c`, `xelp.h`, `xelpcfg.h`) are all that are needed. +(`xelp.c`, `xelp.h`, `xelpcfg.h`) are all that are needed. The library +uses zero dynamic memory (no malloc), supports multiple independent +instances on different serial ports, and compiles with any C89 or later +compiler. Scripts are ROM-able const strings -- the parser never modifies +its input. Function dispatch tables make any C/C++ function callable from +the CLI or from scripts. + +## Key characteristics + +- Pure C (C89 compliant), optional C++ `extern "C"` support +- Instance-based: no globals, multiple independent CLIs on different ports +- Three modes: CLI (line-buffered), KEY (single keypress), THR (pass-through) +- Table-driven parser state machine (8 states, 94-byte lookup table) +- Command history with UP/DOWN arrow recall (optional) +- Line editing: cursor movement, insert, delete (optional) +- Multi-byte ANSI key recognition (arrow keys, Home/End, etc.) +- 4 callee-clobbers-all return registers per instance +- Output control: mute all output, echo masking (password mode) +- Quoted strings, escape sequences, comments, semicolon statement separator +- Hex + decimal number parsing (0xFF, FFh, -123 formats) +- 47 test units, 598 test cases, 100% line coverage +- Fuzz tested with libFuzzer +- BSD 2-Clause license ## Docs +- [AGENTS.md](https://github.com/deftio/xelp/blob/master/AGENTS.md): AI coding agent reference -- function signatures, setup patterns, parser internals, common mistakes, testing patterns - [API Reference](https://github.com/deftio/xelp/blob/master/docs/api-reference.md): All public types, functions, macros, and return codes - [Tutorial](https://github.com/deftio/xelp/blob/master/docs/tutorial.md): Step-by-step introduction with complete code examples - [Configuration Guide](https://github.com/deftio/xelp/blob/master/docs/configuration.md): Compile-time feature flags, buffer sizes, key mappings +- [Build Profiles](https://github.com/deftio/xelp/blob/master/docs/build-profiles.md): Feature system and compile-time options with size impact - [Porting Guide](https://github.com/deftio/xelp/blob/master/docs/porting.md): How to bring up xelp on a new platform -- [Examples](https://github.com/deftio/xelp/blob/master/docs/examples.md): Annotated code for POSIX, Arduino, ESP32, bare-metal, multi-instance -- [AGENTS.md](https://github.com/deftio/xelp/blob/master/AGENTS.md): Concise coding reference for AI agents integrating xelp +- [Examples Guide](https://github.com/deftio/xelp/blob/master/docs/examples.md): Annotated code for POSIX, Arduino, ESP32, bare-metal, multi-instance +- [Testing Guide](https://github.com/deftio/xelp/blob/master/docs/testing.md): JumpBug test framework reference ## Code -- [xelp.h](https://github.com/deftio/xelp/blob/master/src/xelp.h): Public API header -- types, macros, function declarations -- [xelp.c](https://github.com/deftio/xelp/blob/master/src/xelp.c): Implementation (~1100 lines) -- [xelpcfg.h](https://github.com/deftio/xelp/blob/master/src/xelpcfg.h): Compile-time configuration +- [xelp.h](https://github.com/deftio/xelp/blob/master/src/xelp.h): Public API header -- types, macros, function declarations (~450 lines) +- [xelp.c](https://github.com/deftio/xelp/blob/master/src/xelp.c): Implementation -- parser state machine, tokenizer, dispatch, line editing, history (~1141 lines) +- [xelpcfg.h](https://github.com/deftio/xelp/blob/master/src/xelpcfg.h): Compile-time configuration -- feature flags, key mappings, buffer sizes (~207 lines) +- [XelpArduino.h](https://github.com/deftio/xelp/blob/master/src/XelpArduino.h): C++ wrapper class for Arduino integration ## Examples -- [POSIX example](https://github.com/deftio/xelp/blob/master/examples/posix-simple/xelp-example.c): Full featured CLI with ncurses -- [Bare-metal example](https://github.com/deftio/xelp/blob/master/examples/bare-metal/bare-metal-example.c): Minimal embedded template +- [POSIX example](https://github.com/deftio/xelp/blob/master/examples/posix-simple/xelp-example.c): Full interactive CLI with ncurses (Linux/macOS) +- [POSIX C++ example](https://github.com/deftio/xelp/blob/master/examples/posix-simple-cpp/xelp-example-cpp.cpp): C++ wrapper version of POSIX example +- [Bare-metal example](https://github.com/deftio/xelp/blob/master/examples/bare-metal/bare-metal-example.c): Minimal MCU porting template with all three modes - [Multi-instance example](https://github.com/deftio/xelp/blob/master/examples/multi-instance/multi-instance-example.c): Two independent CLIs on different UARTs - [Scripting example](https://github.com/deftio/xelp/blob/master/examples/scripting/scripting-example.c): ROM-able scripts, multi-statement parsing -- [Arduino example](https://github.com/deftio/xelp/blob/master/examples/arduino/arduino.ino): Serial CLI for Arduino boards -- [ESP32 WiFi example](https://github.com/deftio/xelp/blob/master/examples/esp32-wifi/esp32-wifi.ino): CLI over WiFi telnet + serial +- [Arduino example](https://github.com/deftio/xelp/blob/master/examples/arduino/arduino.ino): Serial CLI for Arduino boards (raw C API) +- [Arduino C++ example](https://github.com/deftio/xelp/blob/master/examples/arduino-cpp/arduino-cpp.ino): XelpCLI C++ wrapper class +- [Arduino live CLI](https://github.com/deftio/xelp/blob/master/examples/arduino-live-cli/arduino-live-cli.ino): Interactive Arduino serial CLI +- [ESP32 WiFi example](https://github.com/deftio/xelp/blob/master/examples/esp32-wifi/esp32-wifi.ino): CLI over WiFi telnet + serial, time/weather fetch +- [ESP32-C6 WiFi example](https://github.com/deftio/xelp/blob/master/examples/esp32c6-wifi/): Dual-instance Serial + BLE with WiFi commands, Web Bluetooth terminal +- [ESP32 BLE CLI](https://github.com/deftio/xelp/blob/master/examples/esp32-ble-cli/): Dual-instance Serial + BLE CLI with Web Bluetooth terminal, cross-instance messaging +- [Pico CLI](https://github.com/deftio/xelp/blob/master/examples/pico-cli/): Raspberry Pi Pico pure C (GPIO, ADC, PWM) +- [Pico Arduino CLI](https://github.com/deftio/xelp/blob/master/examples/pico-cli-arduino/): Raspberry Pi Pico with Arduino-Pico core + C++ wrapper + +## Tests + +- [Unit tests](https://github.com/deftio/xelp/blob/master/tests/xelp_unit_tests.c): 47 test units, 598 test cases (JumpBug framework) +- [JumpBug framework](https://github.com/deftio/xelp/blob/master/tests/jumpbug_unit_test_fw.c): Minimal C89-compatible unit test framework +- [Fuzz parse](https://github.com/deftio/xelp/blob/master/tests/fuzz/fuzz_parse.c): libFuzzer harness for XelpParse +- [Fuzz parsekey](https://github.com/deftio/xelp/blob/master/tests/fuzz/fuzz_parsekey.c): libFuzzer harness for XelpParseKey + +## Build & tools -## Optional +- [CMakeLists.txt](https://github.com/deftio/xelp/blob/master/CMakeLists.txt): CMake configuration (ESP-IDF component + plain CMake library) +- [Makefile](https://github.com/deftio/xelp/blob/master/makefile): Development makefile (validate, tests, examples, coverage, fuzz, sizes) +- [Release script](https://github.com/deftio/xelp/blob/master/tools/make_release.sh): Full guided release pipeline +- [Cross-build script](https://github.com/deftio/xelp/blob/master/tools/crossbuild.sh): Docker cross-compilation for 18+ architectures +- [Dockerfile](https://github.com/deftio/xelp/blob/master/tools/Dockerfile.crossbuild): Docker image with 18 cross-compilation toolchains +- [Size profiles](https://github.com/deftio/xelp/blob/master/dev/size_profiles.sh): Feature profile sizing tool +## Package manifests + +- [library.json](https://github.com/deftio/xelp/blob/master/library.json): PlatformIO package manifest +- [library.properties](https://github.com/deftio/xelp/blob/master/library.properties): Arduino Library Manager manifest +- [idf_component.yml](https://github.com/deftio/xelp/blob/master/idf_component.yml): ESP-IDF Component Registry manifest + +## Project docs + +- [README.md](https://github.com/deftio/xelp/blob/master/README.md): Main project README with quick start, size tables, architecture support - [Changelog](https://github.com/deftio/xelp/blob/master/CHANGELOG.md): Version history and migration notes - [Contributing](https://github.com/deftio/xelp/blob/master/CONTRIBUTING.md): Coding standards, branch model, PR guidelines - [Release Management](https://github.com/deftio/xelp/blob/master/release_management.md): Versioning and release workflow +- [License](https://github.com/deftio/xelp/blob/master/LICENSE.txt): BSD 2-Clause license + +## API quick reference + +Core functions: +- `XelpInit(XELP *ths, const char *aboutMsg)` -- initialize instance +- `XelpParseKey(XELP *ths, char key)` -- feed one interactive character +- `XelpParse(XELP *ths, const char *buf, int len)` -- execute script buffer +- `XelpParseXB(XELP *ths, XelpBuf *script)` -- execute script from XelpBuf +- `XelpExecKC(XELP *ths, XELPKEYCODE key)` -- execute single-key command +- `XelpOut(XELP *ths, const char *msg, int maxlen)` -- output string +- `XelpPutc(XELP *ths, char c)` -- output single character +- `XelpHelp(XELP *ths)` -- print help listing + +Argument parsing: +- `XelpArgsInit(XelpArgs *a, const char *args, int len)` -- init iterator +- `XelpNextTok(XelpArgs *a, XelpBuf *tok)` -- get next token +- `XelpNextInt(XelpArgs *a, int *val)` -- get next token as int +- `XelpArgCount(XelpArgs *a, int *n)` -- count tokens (non-destructive) +- `XelpArgInt(const char *args, int len, int n, int *val)` -- random access int +- `XelpArgStr(const char *args, int len, int n, const char **s, int *slen)` -- random access string + +Tokenizer: +- `XelpTokLineXB(XelpBuf *buf, XelpBuf *tok, int type)` -- get next token or line +- `XelpTokN(XelpBuf *buf, int n, XelpBuf *tok)` -- get Nth token +- `XelpNumToks(XelpBuf *buf, int *n)` -- count tokens + +String utilities: +- `XelpStrLen(const char *s)` -- string length +- `XelpStrEq(const char *buf, int len, const char *cmd)` -- compare +- `XelpStrEq2(const char *buf, const char *end, const char *cmd)` -- compare pointer-pair +- `XelpStr2Int(const char *s, int maxlen)` -- parse to int +- `XelpParseNum(const char *s, int maxlen, int *n)` -- parse with result code +- `XelpBufCmp(const char *as, const char *ae, const char *bs, const char *be, int type)` -- compare buffers +- `XelpFindTok(XelpBuf *x, const char *t0s, const char *t0e, int type)` -- find token + +Command function signatures: +- CLI: `XELPRESULT fn(XELP *ths, const char *args, int len)` +- KEY: `XELPRESULT fn(XELP *ths, XELPKEYCODE key)` + +Return codes: XELP_S_OK (0), XELP_W_WARN (1), XELP_S_NOTFOUND (2), +XELP_E_ERR (-1), XELP_E_CMDBUFFULL (-2), XELP_E_CMDNOTFOUND (-3). + +## Compile-time feature flags + +- `XELP_ENABLE_CLI` -- CLI mode + scripting (core, ~1.5-2.6 KB) +- `XELP_ENABLE_LINE_EDIT` -- cursor movement, insert, delete (~800-1000 bytes) +- `XELP_ENABLE_HISTORY` -- UP/DOWN command recall (~420 bytes, requires LINE_EDIT) +- `XELP_ENABLE_KEY` -- single keypress mode (~200-500 bytes) +- `XELP_ENABLE_THR` -- pass-through mode (~50-125 bytes) +- `XELP_ENABLE_HELP` -- built-in help listing (~180-350 bytes) +- `XELP_ENABLE_FULL` -- all of the above (except history) +- `XELP_CONFIG_OVERRIDE` -- use project-specific xelp_ovr.h for config diff --git a/pages/examples.html b/pages/examples.html index 080b337..b59a09d 100644 --- a/pages/examples.html +++ b/pages/examples.html @@ -39,7 +39,10 @@

Examples

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 +Arduino Live CLIAny Arduino boardFull hardware CLI: GPIO, ADC, PWM, tone, pulse ESP32 WiFiESP32WiFi config, time and weather fetch via CLI +ESP32-C6 WiFi + BLEESP32-C6Dual-CLI: Serial + BLE with WiFi commands +ESP32 BLE CLIAny ESP32 with BLEDual-CLI: Serial + BLE with GPIO, cross-instance messaging 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 @@ -231,6 +234,29 @@

Commands

+

Arduino Live CLI

+ +

File: +examples/arduino-live-cli/arduino-live-cli.ino

+ +

Full interactive hardware CLI using the C++ wrapper class. Over 15 commands +for GPIO, ADC, PWM, tone, pulse width measurement, and pin scanning. Works +on any Arduino-compatible board — AVR, ARM, ESP32, RP2040.

+ +

Commands

+
    +
  • help / ? — list all commands
  • +
  • setpin <pin> <0|1> / getpin <pin> — digital I/O
  • +
  • pinmode <pin> <in|out|pullup> — configure pin direction
  • +
  • setpwm <pin> <0-255> / readadc <pin> — analog I/O
  • +
  • tone <pin> <freq> / notone <pin> — generate tones
  • +
  • pulsein <pin> — measure pulse width
  • +
  • scanpins <start> <end> — scan GPIO pin states
  • +
  • reboot — software reset
  • +
+ + +

Arduino C++

File: @@ -264,6 +290,69 @@

Commands

+

ESP32-C6 WiFi + BLE

+ +

Files: +examples/esp32c6-wifi/

+ +

Dual-instance demo for the Seeed XIAO ESP32-C6: one CLI on USB Serial, +one on BLE (Nordic UART Service). WiFi commands for scanning and connecting. +Includes a Web Bluetooth terminal in web/index.html.

+ +

Commands

+
    +
  • help — list commands
  • +
  • ssid <name> / wifipass <pw> — set WiFi credentials
  • +
  • connect / disconnect — manage WiFi
  • +
  • status — show WiFi and BLE status
  • +
  • led <0|1> / adc — hardware control
  • +
+ + + +

ESP32 BLE CLI

+ +

Files: +examples/esp32-ble-cli/

+ +

Dual-instance CLI demo for any BLE-capable ESP32 (ESP32, ESP32-S3, +ESP32-C3, ESP32-C6, ESP32-H2). One xelp instance on USB Serial, one on +BLE (Nordic UART Service), both sharing the same command table. Includes +a Web Bluetooth terminal, Makefile, and cross-instance messaging.

+ +

Commands

+
    +
  • help / ? — list commands
  • +
  • info — app version, board type, uptime, xelp version
  • +
  • led <0|1> — built-in LED (auto-detects RGB NeoPixel)
  • +
  • rgb <r> <g> <b> — set RGB LED color (boards with NeoPixel)
  • +
  • setpin / getpin / pinmode — digital I/O
  • +
  • setpwm / readadc — analog I/O
  • +
  • status — Serial, BLE, LED type, uptime
  • +
  • sendmsg <serial|ble> <text> — cross-instance messaging
  • +
  • reboot — software reset
  • +
+ +

Architecture

+ +
USB Serial                          BLE (Nordic UART Service)
+    |                                       |
+    v                                       v
+XelpParseKey(&cliSerial, ch)     XelpParseKey(&cliBle, ch)
+    |                                       |
+    +------ shared command table -----------+
+    |                                       |
+    v                                       v
+serialOut(char c)                  bleOut(char c)
+ -> Serial.write()                  -> bleTxBuf[] -> drip-feed notify()
+
+ +

Both instances share a single XELPCLIFuncMapEntry[] command table +but have separate output functions, buffers, and history state. +Cross-instance messaging is supported via sendmsg.

+ + +

Scripting

File: diff --git a/pages/index.html b/pages/index.html index 5f4c168..46b97dd 100644 --- a/pages/index.html +++ b/pages/index.html @@ -221,6 +221,8 @@

Documentation

  • Porting Guide — how to bring up xelp on a new platform
  • Release History
  • Contributing
  • +
  • AGENTS.md — AI coding agent reference (function signatures, parser internals, setup patterns)
  • +
  • llms.txt — project overview for LLMs (llmstxt.org format)
  • License

    diff --git a/src/xelp.c b/src/xelp.c index df3ed7a..945c9b7 100755 --- a/src/xelp.c +++ b/src/xelp.c @@ -46,13 +46,26 @@ static void _xelp_putc(XELP *ths, char c) { #define _PUTC(c) _xelp_putc(ths, (c)) #define _XOUTC(x,c) _xelp_putc((x), (c)) +#ifdef XELP_ENABLE_CLI static void _xelp_echo(XELP *ths, char c) { if (!ths->mOutEnable || !ths->mpfOut) return; if (ths->mEchoChar == XELP_ECHO_NORMAL) ths->mpfOut(c); else if (ths->mEchoChar != XELP_ECHO_OFF) ths->mpfOut(ths->mEchoChar); } #define _ECHO(c) _xelp_echo(ths, (c)) +#endif + +/* Enter key test for interactive mode — respects XELP_ENTER_CR / XELP_ENTER_LF + from xelpcfg.h. Only used in XelpParseKey; script parsing is unaffected. */ +#if defined(XELP_ENTER_CR) && defined(XELP_ENTER_LF) +#define _XELP_IS_ENTER(ch) ((ch) == '\n' || (ch) == '\r') +#elif defined(XELP_ENTER_CR) +#define _XELP_IS_ENTER(ch) ((ch) == '\r') +#else +#define _XELP_IS_ENTER(ch) ((ch) == '\n') +#endif +#if defined(XELP_ENABLE_CLI) && defined(XELP_ENABLE_LINE_EDIT) static void _xelp_cursor(XELP *ths, char c) { if (ths->mEchoChar != XELP_ECHO_OFF && ths->mOutEnable && ths->mpfOut) ths->mpfOut(c); @@ -75,6 +88,7 @@ static void _xelp_memmove(char *dst, const char *src, int n) { while (src > e) *--dst = *--src; } } +#endif /***************************************** _xelpKeyAccum() - key-input accumulator state machine. @@ -268,7 +282,7 @@ static void _xelpHistRecall(XELP *ths, int dir) { } #endif /* XELP_ENABLE_CLI && XELP_ENABLE_LINE_EDIT && XELP_ENABLE_HISTORY */ -#ifdef XELP_ENABLE_HELP +#if defined(XELP_ENABLE_HELP) && defined(XELP_ENABLE_KEY) /***************************************** _xelpPrintKeyName() - print human-readable name for a keycode in help output */ @@ -345,7 +359,7 @@ XELPRESULT XelpHelp(XELP* ths) XelpOut(ths,XELP_HELP_KEY_STR,x); do { _xelpPrintKeyName(ths, e->mKey); - _PUTC(':'); + _PUTC('\t'); XelpOut(ths,e->mpHelpString,x); _PUTC('\n'); e++; @@ -357,8 +371,8 @@ XELPRESULT XelpHelp(XELP* ths) XelpOut(ths,XELP_HELP_CLI_STR,x); do { XelpOut(ths,s->mpCmd,x); - _PUTC(':'); - XelpOut(ths,s->mpHelpString,x); + _PUTC('\t'); + XelpOut(ths,s->mpHelpString,x); _PUTC('\n'); s++; } while (s->mFunPtr); @@ -1007,7 +1021,7 @@ XELPRESULT XelpParseKey (XELP *ths, char key) _CURSOR('\b'); _xelpRedrawFromCursor(ths); } - } else if (ch == XELPKEY_ENTER) { + } else if (_XELP_IS_ENTER(ch)) { _xelpHandleEnter(ths); } else if (ch >= 0x20 && ch <= 0x7E) { /* printable character */ @@ -1045,7 +1059,7 @@ XELPRESULT XelpParseKey (XELP *ths, char key) if (ths->mpfBksp) ths->mpfBksp(); } - } else if (ch == XELPKEY_ENTER) { + } else if (_XELP_IS_ENTER(ch)) { _xelpHandleEnter(ths); } else { _ECHO(ch); diff --git a/src/xelpcfg.h b/src/xelpcfg.h index 87f975e..4cf988f 100755 --- a/src/xelpcfg.h +++ b/src/xelpcfg.h @@ -1,10 +1,10 @@ /** @xelpcfg.h - header file for xelp command interpreter - + @copy Copyright (C) <2011> @author M A Chatterjee - + This file contains build-flags used to control what features are used in the xelp command library compilation. comment in or out to reduce libary size or set configuration such as key map. @@ -14,34 +14,46 @@ #ifndef __XELP_CONFIG_H__ #define __XELP_CONFIG_H__ -#ifdef XELP_CONFIG_OVERRIDE -/* - To use your own configuration without modifying this file: - 1. Define XELP_CONFIG_OVERRIDE in your compiler flags (-DXELP_CONFIG_OVERRIDE) - 2. Create xelp_ovr.h in your include path with the defines you need - 3. Any XELP_XXX define not set in xelp_ovr.h will use the default from xelp.h - This keeps the library source untouched across updates. - See docs/build-profiles.md for details and examples. -*/ -#include "xelp_ovr.h" -#else /* use the defaults below */ - /**************************************************************************************************** Key mappings. Use these key mappings to switch modes at the command line or in Key or Thru Modes. - any valid character is allowed. e.g. - #define XELPKEY_CLI ('c') + any valid character is allowed. e.g. + #define XELPKEY_CLI ('c') */ +#ifndef XELPKEY_CLI #define XELPKEY_CLI (XELPKEY_CTP) /* enter command mode */ +#endif +#ifndef XELPKEY_KEY #define XELPKEY_KEY (XELPKEY_ESC) /* enter single key mode */ +#endif +#ifndef XELPKEY_THR #define XELPKEY_THR (XELPKEY_CTT) /* enter thru mode */ +#endif + +/**************************************************************************************************** + Enter key detection for interactive mode (XelpParseKey). + XELP_ENTER_LF: accept LF (\n, 0x0A) as ENTER. + XELP_ENTER_CR: accept CR (\r, 0x0D) as ENTER. + Enable both to accept either character — recommended for cross-platform use. + Only affects interactive input (XelpParseKey); script parsing always uses \n. + */ +#ifndef XELP_ENTER_LF +#define XELP_ENTER_LF 1 +#endif +#ifndef XELP_ENTER_CR +#define XELP_ENTER_CR 1 +#endif /**************************************************************************************************** Escape character mappings: used to skip symbols that otherwise might be parsed such as ; or " */ +#ifndef XELP_CLI_ESC #define XELP_CLI_ESC ('`') /* character used for escaping at command line or in script */ +#endif +#ifndef XELP_QUO_ESC #define XELP_QUO_ESC ('\\') /* character used for escaping inside quoted strings */ +#endif @@ -50,7 +62,9 @@ definining this flag includes support for interactive command line. Script support also requires this. */ +#ifndef XELP_ENABLE_CLI #define XELP_ENABLE_CLI 1 +#endif /**************************************************************************************************** Enable Line Editing in CLI Mode. @@ -59,7 +73,9 @@ Requires XELP_ENABLE_CLI. Adds ~800-1000 bytes on ARM Thumb. When not defined, CLI uses append-only input with mpfBksp callback. */ +#ifndef XELP_ENABLE_LINE_EDIT #define XELP_ENABLE_LINE_EDIT 1 +#endif /**************************************************************************************************** Enable Command History (UP/DOWN arrow recall). @@ -70,7 +86,9 @@ RAM cost: XELP_HIST_DEPTH * XELP_CMDBUFSZ + XELP_CMDBUFSZ + 4 bytes per instance. Code cost: ~420 bytes on ARM Thumb (-Os). */ +#ifndef XELP_ENABLE_HISTORY #define XELP_ENABLE_HISTORY 1 +#endif /**************************************************************************************************** @@ -79,7 +97,9 @@ leaving undefined saves btw 200-500 bytes (target dependant) */ +#ifndef XELP_ENABLE_KEY #define XELP_ENABLE_KEY 1 +#endif /**************************************************************************************************** @@ -89,38 +109,50 @@ leaving undefined saves btw 50-125 bytes (target dependant) */ -#define XELP_ENABLE_THR 1 +#ifndef XELP_ENABLE_THR +#define XELP_ENABLE_THR 1 +#endif /**************************************************************************************************** - Compile built-in help function. + Compile built-in help function. Leaving undefined saves ~180-350 bytes. (target dependant) XELP_HELP_XXX_STR are the strings used to prefix sections in the online help. See examples or docs */ -#define XELP_ENABLE_HELP 1 +#ifndef XELP_ENABLE_HELP +#define XELP_ENABLE_HELP 1 +#endif /**************************************************************************************************** - Help related controls + Help related controls */ +#ifndef XELP_HELP_KEY_STR #define XELP_HELP_KEY_STR "\nKey functions\n" /* Help section for single-key press commands such as menus */ +#endif +#ifndef XELP_HELP_CLI_STR #define XELP_HELP_CLI_STR "\nCLI functions\n" /* Help string displayed before script or CLI commands */ +#endif +#ifndef XELP_HELP_ABT_STR #define XELP_HELP_ABT_STR (ths->mpAboutMsg) /* You may set to any null terminated string e.g. "My Embedded System About Message" */ +#endif -/**************************************************************************************************** - prompt string, leave undefined (commented out) for no prompt and to save space - if a fixed string is provided such as "xelp>" then all instances will use this prompt. - if set to (ths->mpPrompt) then per-instance console prompt is set via pointer. (see examples) This can be usesful when different +/**************************************************************************************************** + prompt string, leave undefined (commented out) for no prompt and to save space + if a fixed string is provided such as "xelp>" then all instances will use this prompt. + if set to (ths->mpPrompt) then per-instance console prompt is set via pointer. (see examples) This can be usesful when different instances are listening on different ports and each should have a different prompt. - for a per-instance prompt (eg each instance of the xelp interpreter running presents a different prompt such as ser1> and ser2> ) + for a per-instance prompt (eg each instance of the xelp interpreter running presents a different prompt such as ser1> and ser2> ) use this: - + #define XELP_CLI_PROMPT (ths->mpPrompt) - Then use the macros in Xelp.h + Then use the macros in Xelp.h XELP_SET_VAL_CLI_PROMPT(myXelp,"yourPrompt") //myXelp is the instance variable, message will be only for that instance. */ -#define XELP_CLI_PROMPT "xelp>" +#ifndef XELP_CLI_PROMPT +#define XELP_CLI_PROMPT "xelp>" +#endif /**************************************************************************************************** @@ -129,9 +161,13 @@ R1-R3: command-specific return values (xelp engine never touches these). Minimum is 4. Size of each register is set by XELPREG (default is machine int). */ +#ifndef XELP_REGS_SZ #define XELP_REGS_SZ 4 +#endif +#ifndef XELPREG #define XELPREG int /* can change this to a valid C type for your plaform. eg. short, long, _int64 */ +#endif /** @@ -142,6 +178,29 @@ /* #define XELP_ENABLE_FULL 1 */ -#endif /* XELP_CONFIG_OVERRIDE (not used)*/ +/**************************************************************************************************** + XELP_CONFIG_OVERRIDE -- customize the library without modifying source files. + + To use your own configuration: + 1. Define XELP_CONFIG_OVERRIDE in your compiler flags (-DXELP_CONFIG_OVERRIDE) + 2. Create xelp_ovr.h in your include path + 3. Use #undef to disable features or #undef + #define to change values. + Anything you don't touch keeps its default from above. + + xelp_ovr.h is included AFTER the defaults so that #undef works correctly. + + Example xelp_ovr.h -- per-instance prompt and no thru mode: + + #undef XELP_CLI_PROMPT + #define XELP_CLI_PROMPT (ths->mpPrompt) + + #undef XELP_ENABLE_THR + + See docs/build-profiles.md for more examples. + */ +#ifdef XELP_CONFIG_OVERRIDE +#include "xelp_ovr.h" +#endif + #endif /* __XELP_CONFIG_H__ */ From 3516efc544c13b37d635125fed10850283aaf329 Mon Sep 17 00:00:00 2001 From: deftio Date: Mon, 11 May 2026 04:24:15 -0700 Subject: [PATCH 3/3] sync manifests, badges, and sizes for 0.3.2 --- README.md | 40 ++++++++++++++++++++-------------------- library.properties | 2 +- pages/index.html | 40 ++++++++++++++++++++-------------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index c6b9735..53ce361 100644 --- a/README.md +++ b/README.md @@ -248,26 +248,26 @@ features). Even the largest full build is under 7 KB. | CPU | Width | Compiler | KEY (bytes) | CLI (bytes) | FULL (bytes) | |-----|------:|----------|------------:|------------:|-------------:| -| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4270 | 4328 | -| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4370 | 4428 | -| Z80 | 8 | SDCC | 2121 | 7287 | 7395 | -| 6800 (HC08) | 8 | SDCC | 2471 | 8616 | 8718 | -| MSP430 | 16 | msp430-gcc | 770 | 3486 | 3532 | -| 68HC11 | 16 | m68hc11-gcc | 2369 | 6895 | 6966 | -| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2600 | 2632 | -| ARM Thumb | 32 | arm-none-eabi-gcc | 580 | 2598 | 2642 | -| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 3100 | 3138 | -| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2947 | 2979 | -| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3336 | 3384 | -| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3934 | 3994 | -| x86-32 | 32 | GCC | 1081 | 4919 | 4969 | -| MIPS32 | 32 | mipsel-linux-gnu-gcc | 1296 | 5224 | 5272 | -| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 6066 | 6130 | -| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3554 | 3588 | -| x86-64 | 64 | Clang | 1043 | 5269 | 5311 | -| x86-64 | 64 | GCC | 1063 | 5138 | 5187 | -| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5574 | 5630 | -| MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 1360 | 5864 | 5928 | +| AVR (ATtiny85) | 8 | avr-gcc | 5174 | 5174 | 5174 | +| AVR (ATmega328P) | 8 | avr-gcc | 5284 | 5284 | 5284 | +| Z80 | 8 | SDCC | 8386 | 8386 | 8386 | +| 6800 (HC08) | 8 | SDCC | 10078 | 10078 | 10078 | +| MSP430 | 16 | msp430-gcc | 4272 | 4272 | 4272 | +| 68HC11 | 16 | m68hc11-gcc | 8701 | 8701 | 8701 | +| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 3060 | 3060 | 3060 | +| ARM Thumb | 32 | arm-none-eabi-gcc | 3074 | 3074 | 3074 | +| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 3447 | 3447 | 3447 | +| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 3654 | 3654 | 3654 | +| m68k | 32 | m68k-linux-gnu-gcc | 4044 | 4044 | 4044 | +| ARM32 | 32 | arm-none-eabi-gcc | 4638 | 4638 | 4638 | +| x86-32 | 32 | GCC | 5628 | 5628 | 5628 | +| MIPS32 | 32 | mipsel-linux-gnu-gcc | 6056 | 6056 | 6056 | +| PowerPC | 32 | powerpc-linux-gnu-gcc | 6842 | 6842 | 6842 | +| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 4136 | 4136 | 4136 | +| x86-64 | 64 | GCC | 5935 | 5935 | 5935 | +| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 6282 | 6282 | 6282 | +| x86-64 | 64 | Clang | 6495 | 6495 | 6495 | +| MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 6904 | 6904 | 6904 | x86-64 GCC row is measured directly; others from cross-compilation via diff --git a/library.properties b/library.properties index 1a24ace..dbdfb23 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=xelp -version=0.3.2 +version=0.3.2 author=M. A. Chatterjee maintainer=M. A. Chatterjee sentence=Tiny CLI and script interpreter for embedded systems. 2KB, zero malloc, multi-instance. diff --git a/pages/index.html b/pages/index.html index 46b97dd..5caa27a 100644 --- a/pages/index.html +++ b/pages/index.html @@ -171,26 +171,26 @@

    Compiled sizes

    - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    CPUWidthCompilerKEY (bytes)CLI (bytes)FULL (bytes)
    AVR (ATtiny85)8avr-gcc104642704328
    AVR (ATmega328P)8avr-gcc105443704428
    Z808SDCC212172877395
    6800 (HC08)8SDCC247186168718
    MSP43016msp430-gcc77034863532
    68HC1116m68hc11-gcc236968956966
    Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc57626002632
    ARM Thumb32arm-none-eabi-gcc58025982642
    RISC-V (rv32)32riscv64-unknown-elf-gcc72231003138
    Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc72329472979
    m68k32m68k-linux-gnu-gcc72833363384
    ARM3232arm-none-eabi-gcc98039343994
    x86-3232GCC108149194969
    MIPS3232mipsel-linux-gnu-gcc129652245272
    PowerPC32powerpc-linux-gnu-gcc150460666130
    RISC-V (rv64)64riscv64-linux-gnu-gcc75635543588
    x86-6464Clang104352695311
    x86-6464GCC106351385187
    AArch64 (ARM64)64aarch64-linux-gnu-gcc132455745630
    MIPS6464mips64el-linux-gnuabi64-gcc136058645928
    AVR (ATtiny85)8avr-gcc517451745174
    AVR (ATmega328P)8avr-gcc528452845284
    Z808SDCC838683868386
    6800 (HC08)8SDCC100781007810078
    MSP43016msp430-gcc427242724272
    68HC1116m68hc11-gcc870187018701
    Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc306030603060
    ARM Thumb32arm-none-eabi-gcc307430743074
    Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc344734473447
    RISC-V (rv32)32riscv64-unknown-elf-gcc365436543654
    m68k32m68k-linux-gnu-gcc404440444044
    ARM3232arm-none-eabi-gcc463846384638
    x86-3232GCC562856285628
    MIPS3232mipsel-linux-gnu-gcc605660566056
    PowerPC32powerpc-linux-gnu-gcc684268426842
    RISC-V (rv64)64riscv64-linux-gnu-gcc413641364136
    x86-6464GCC593559355935
    AArch64 (ARM64)64aarch64-linux-gnu-gcc628262826282
    x86-6464Clang649564956495
    MIPS6464mips64el-linux-gnuabi64-gcc690469046904