GithubHelp home page GithubHelp logo

funbiscuit / embedded-cli Goto Github PK

View Code? Open in Web Editor NEW
215.0 10.0 38.0 785 KB

Single-header CLI with history and autocompletion for embedded systems (like Arduino or STM32)

License: MIT License

CMake 20.90% C++ 36.73% C 41.33% Python 1.03%
arduino c cli embedded iot stm32 command command-line embedded-cli single-header

embedded-cli's Introduction

Embedded CLI

codecov

Note

This library is in maintenance mode. Take a look at full rewrite in Rust: embedded-cli-rs. It has all features of this library and more: utf-8 support, lower memory usage, static dispatch and argument parsing.

Single-header CLI library intended for use in embedded systems (like STM32 or Arduino).

Arduino Demo

Features

  • Dynamic or static allocation
  • Configurable memory usage
  • Command-to-function binding with arguments support
  • Live autocompletion (see demo above, can be disabled)
  • Tab (jump to end of current autocompletion) and backspace (remove char) support
  • History support (navigate with up and down keypress)
  • Limited cursor support (navigate inside input with left and right keypress)
  • Any byte-stream interface is supported (for example, UART)
  • Single-header distribution

Integration

  • You'll need to download embedded_cli.h single-header version from Releases tab. Or you can build it yourself (run build-shl.py python script while inside lib directory)
  • Add embedded_cli.h to your project (to one of the include directories)
  • Include embedded_cli.h header file in your code where you need cli functionality:
#include "embedded_cli.h"
  • In one (and only one) compilation unit (.c/.cpp file) define macro to unwrap implementations:
#define EMBEDDED_CLI_IMPL
#include "embedded_cli.h"

Initialization

To create CLI, you'll need to provide a desired config. Best way is to get default config and change desired values. For example, change maximum amount of command bindings:

EmbeddedCliConfig *config = embeddedCliDefaultConfig();
config->maxBindingCount = 16;

Create an instance of CLI:

EmbeddedCli *cli = embeddedCliNew(config);

If default arguments are good enough for you, you can create cli with default config:

EmbeddedCli *cli = embeddedCliNewDefault();

Provide a function that will be used to send chars to the other end:

void writeChar(EmbeddedCli *embeddedCli, char c);
// ...
cli->writeChar = writeChar;

After creation, provide desired bindings to CLI (can be provided at any point in runtime):

embeddedCliAddBinding(cli, {
        "get-led",          // command name (spaces are not allowed)
        "Get led status",   // Optional help for a command (NULL for no help)
        false,              // flag whether to tokenize arguments (see below)
        nullptr,            // optional pointer to any application context
        onLed               // binding function 
});
embeddedCliAddBinding(cli, {
        "get-adc",
        "Read adc value",
        true,
        nullptr,
        onAdc
});

Don't forget to create binding functions as well:

void onLed(EmbeddedCli *cli, char *args, void *context) {
    // use args as raw null-terminated string of all arguments
}
void onAdc(EmbeddedCli *cli, char *args, void *context) {
    // use args as list of tokens
}

CLI has functions to easily handle list of space separated arguments. If you have null-terminated string you can convert it to list of tokens with single call:

embeddedCliTokenizeArgs(args);

Note: This function will need to make args string double null-terminated Initial array will be modified (so it must be non const), do not use it after this call directly. After that you can count arguments or get single argument as a null-terminated string:

const char * arg = embeddedCliGetToken(args, pos); // args are counted from 1 (not from 0)

uint8_t pos = embeddedCliFindToken(args, argument);

uint8_t count = embeddedCliGetTokenCount(const char *tokenizedStr);

Examples of tokenization:

Input Arg 1 Arg 2 Comments
abc def abc def Space is treated as arg separator
"abc def" abc def To use space inside arg surround arg with quotes
"abc\" d\\ef" abc" d\ef To use quotes or slashes escape them
"abc def" test abc def test You can mix quoted args and non quoted
"abc def"test abc def test Space between quoted args is optional
"abc def""test 2" abc def test 2 Space between quoted args is optional

Runtime

At runtime you need to provide all received chars to cli:

// char c = Serial.read();
embeddedCliReceiveChar(cli, c);

This function can be called from normal code or from ISRs (but don't call it from multiple places). This call puts char into internal buffer, but no processing is done yet.

To do all the "hard" work, call process function periodically

embeddedCliProcess(cli);

Processing should be called from one place only and it shouldn't be inside ISRs. Otherwise, your internal state might get corrupted.

Static allocation

CLI can be used with statically allocated buffer for its internal structures. Required size of buffer depends on CLI configuration. If size is not enough, NULL is returned from embeddedCliNew. To get required size (in bytes) for your config use this call:

uint16_t size = embeddedCliRequiredSize(config);

On some architectures (for example, on some ARM devices) it is important that allocated buffer is aligned properly. Because of that, cliBuffer uses type specific for used platform (16bit on AVR, 32bit on ARM, 64bit on amd64). Actual integer type used to build cliBuffer is defined in macro CLI_UINT. Note that required size in bytes should be divided by size of CLI_UINT, or you can use macro BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE). Create a buffer of required size and provide it to CLI:

CLI_UINT cliBuffer[BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE)];
// ...
config->cliBuffer = cliBuffer;
config->cliBufferSize = CLI_BUFFER_SIZE;

If cliBuffer in config is NULL, dynamic allocation (with malloc) is used. In such case size is computed automatically.

User Guide

You'll need to begin communication (usually through a UART) with a device running a CLI. Terminal is required for correct experience. Following control sequences are reserved:

  • \r or \n sends a command (\r\n is also supported)
  • \b removes last typed character
  • \t moves cursor to the end of autocompleted command
  • Esc[A (key up) and Esc[B (key down) navigates through history
  • Esc[C (key right) and Esc[D (key left) moves the cursor left and right

If you run CLI through a serial port (like on Arduino with its UART-USB converter), you can use for example PuTTY (Windows) or XTerm (Linux).

Examples

There is an example for Arduino (tested with Arduino Nano, but should work on anything with at least 1kB of RAM). Look inside examples directory for a full code.

embedded-cli's People

Contributors

1saeed avatar ccrome avatar funbiscuit avatar icamaster avatar junbo-zheng avatar ldmi3i avatar mtgstuber avatar neusaap avatar olmanqj avatar seanalling-dojofive avatar ymkim92 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

embedded-cli's Issues

Limited command-name-length

There seems to be a limit on the length of the command-name. Is there a way to increase this length beyond 15 characters?

Test both versions of library

Currently only normal distribution (.h+.c files) is tested in CI. Single header build is not tested.
In tests we should test single header version (if it was build) or nomal version (if single header not configured). Single header version will be configurable via BUILD_SINGLE_HEADER in #26
In CI both versions must be tested.

Add support for removal of bindings

Currently it is possible to add bindings at runtime without any issues. It will add more flexibility if there was an option to also remove bindings at runtime.

additional features

It would be nice if the equals sign, or other operators could be identified as tokens, the problem would be that there might be no space character, 1+2 would be one token compared to 1 + 2, which would be 3 tokens.
If you did this, too, the argument list could not be edited on top of itself during the tokenizing process because the tokens would end up longer than the input string, destroying the input string before tokenizing completes.

Just an idea, I'm going down this path using your code as a starting point.
Please let me know what you think,

Invalid CMake settings for compiling as a library and using C

Describe the bug

Using a CMake project configured for C source code only, CMAKE_CXX_COMPILER_ID is undefined resulting in
a warning being printed to terminal, "Can't enable extra flags for compiler ".

Compiler used for project is arm-none-eabi-gcc

To Reproduce

Assuming embedded-cli is a git submodule, from root CMakeLists.txt, include add_subdirectory(embedded-cli) line to build embedded-cli as a library.

Generate CMake environment. During generation, CMake will print warning "Can't enable extra flags for compiler ".

Expected behavior

No warning should be printed, and embedded-cli flags should be added to GNU compiler.

Resolution

Embedded-cli is configured as a C project

project(embedded_cli C)

The above does not include CXX, as such, CMAKE_CXX_COMPILER_ID will never be populated.

Two methods can be used to resolve bug:

  1. Add CXX to project
    project(embedded_cli C CXX)
    
  2. Change CMAKE_CXX_COMPILER_ID to CMAKE_C_COMPILER_ID

Option 2 should be preffered as embedded-cli uses C source files and not C++.

Make Auto Completion Optional

Hi,
could you please make auto completion optional by a flag e.g. in the NewEmbeddedCLI construction and/or an activation/deactivation function?
This would help to be able to use the interface for human operators at the start and switch to e.g. scripted commands from a computer alter.

Cheers
Philipp

Compiler warning when compiled with -Wall

I pulled your code into a project I'm writing now and it works great, thanks for providing this, it was exactly what I was looking for.

Compiling the code with a old gcc 5.4.0 cross compiler with the -Wall flag gives one warning:

embedded-cli/lib/src/embedded_cli.c:7:20: warning: unused variable 'impl' [-Wunused-variable]
EmbeddedCliImpl* impl = (EmbeddedCliImpl*)t->_impl;
^
embedded-cli/lib/src/embedded_cli.c:803:5: note: in expansion of macro 'PREPARE_IMPL'
PREPARE_IMPL(cli)
^

It looks like the PREPARE_IMPL macro does not need to be called in the initInternalBindings() routine.

Thanks!

Argument parsing

Hi.

Your library is very cool. I'm using it in my hobby project in ESP-IDF. Can you solve somehow the quoted arguments?

mycmd "this is my arg" 123
//---
//command is mycmd, first arg is "this is my arg" and other is 123.

This would be nice.

Length of buffer can cause overwritting config struct

CLI_UINT cliBuffer[BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE)];

I used sligthly edited code from example in nrf52840 build. I think I was able to spot potential bug.

Using: BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE) for static allocation of cliBuffer, and totalSize in embeddedCliNew for memset can cause overwritting config struct with zeros. It doesn't cause problems as long as buffer is allocated in memory after struct.

In case buffer is allocated before struct it can set zeros to entire config struct while calling embeddedCliNew due to starting address of cliBuffer being only BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE) bytes before config struct.

It happened on nrf52840. I could neither isolate any causing allocation issue compiler flags, nor replicate it in other systems.

Migrate to catch v3

Currently library uses old single-header version of catch2. Catch2 v3 is released, we should migrate to it.

Make single header build optional in CMakeLists

In CMakeLists for library single-header build should be optional (since it requires python) and disabled by default.
It should be configurable via BUILD_SINGLE_HEADER option e.g. cmake -DBUILD_SINGLE_HEADER=On

Add onEmpty command binding

Currently on any user input something is called - either registered binding or general onCommand when given command is not registered. The only exception is if input empty (user pressed "enter" multiple times). In this case nothing is called.
Add extra optional command binding - onEmpty which will be called when entered command is empty (or contains only whitespace).

Initial request by @Jeff-RavenLabs in #18

Building library on Windows generates error

Describe the bug

When building CMake on Windows, the following is output to console:

  Target "embedded_cli_win32" links to:

    EmbeddedCLI::SingleHeader

  but the target was not found.  Possible reasons include:

    * There is a typo in the target name.
    * A find_package call is missing for an IMPORTED target.
    * An ALIAS target is missing.

To Reproduce

  1. Clone repository to a folder named embedded-cli
  2. cd embedded cli
  3. mkdir build
  4. cd build
  5. cmake -G "Ninja" ..

Expected behavior

Example programs should not be generated by default, example applications should only generate if user has explicitly asked to generate test application.

Add access to raw input inside command binding

Example:

void onAdc(EmbeddedCli *cli, char *args, void *context) {
    // somehow get input
   while (embeddedCliBytesAvailable(cli) > 0) {
       uint8_t b = embeddedCliReadByte(cli);
   }
}

If you input is read from cli, it will not be then processed as command. For example suppose command binding for get-adc and following input is given:

get-adc
get-adc
get-adc

If in binding get-adc 3 bytes are read, then cli will see following:

get-adc // call binding get-adc
-adc // call unknown command "-adc" ("get" was read by user inside binding)
get-adc // call binding get-adc

Another important thing is that this will work correctly only if raw input is provided to cli only inside ISR. Otherwise new input will not be added while command binding is executing (unless it is added it manually, but that's a bad design)

Initial request by @windsunsjtu in #15

CLI options getopt

Hello.

I am using this cli in an embedded c++ project, and it's pretty cool.
I found this getopt port and I could eventually use it by iterating on the args.

When in it comes to help, it would be great to have the possibilty to pass a string documenting the CLI args and options.
This way say we have a short description to summarize the purpose of the command, and a long one that is printed when we call help on this command.

Namely

#define LED_CMD_SYNTAX  "\
led [-p <led_id>] [<arg>]\n\
\t -p <led_id>: led id, must be 0, 1 or 2. defaults to 0.\n\
\t <arg>: toggle,on or off. defaults to toggle\
"

 CliCommandBinding led_binding = {
          .name = "led",
          .help= "Led manipulation command", // this is the short help
          .syntax=LED_CMD_SYNTAX,  // this is the long help, can be NULL if not used
          .tokenizeArgs = true,
          .context = NULL,
          .binding = onLed
  }; 

Expected output:

$ help
 * help
        Print list of commands
 * clear
        Clears the console
 * led
        Led manipulation command
$ help led
 * led
        Led manipulation command. 
        Syntax:
        led [-p <led_id>] [<arg>]
           -p <led_id>: led id, must be 0, 1 or 2. defaults to 0.
           <arg>: toggle,on or off. defaults to toggle

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.