2022-11-18 21:38:03 +07:00
|
|
|
/**
|
|
|
|
* This header was automatically built using
|
|
|
|
* embedded_cli.h and embedded_cli.c
|
|
|
|
* @date 2022-11-03
|
|
|
|
*
|
|
|
|
* MIT License
|
|
|
|
*
|
|
|
|
* Copyright (c) 2021 Sviatoslav Kokurin (funbiscuit)
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
* SOFTWARE.
|
|
|
|
*/
|
|
|
|
#ifndef EMBEDDED_CLI_H
|
|
|
|
#define EMBEDDED_CLI_H
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef __cplusplus
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
#else
|
|
|
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// cstdint is available only since C++11, so use C header
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
|
|
// used for proper alignment of cli buffer
|
|
|
|
#if UINTPTR_MAX == 0xFFFF
|
|
|
|
#define CLI_UINT uint16_t
|
|
|
|
#elif UINTPTR_MAX == 0xFFFFFFFF
|
|
|
|
#define CLI_UINT uint32_t
|
|
|
|
#elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu
|
|
|
|
#define CLI_UINT uint64_t
|
|
|
|
#else
|
|
|
|
#error unsupported pointer size
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#define CLI_UINT_SIZE (sizeof(CLI_UINT))
|
|
|
|
// convert size in bytes to size in terms of CLI_UINTs (rounded up
|
|
|
|
// if bytes is not divisible by size of single CLI_UINT)
|
|
|
|
#define BYTES_TO_CLI_UINTS(bytes) \
|
|
|
|
(((bytes) + CLI_UINT_SIZE - 1)/CLI_UINT_SIZE)
|
|
|
|
|
|
|
|
typedef struct CliCommand CliCommand;
|
|
|
|
typedef struct CliCommandBinding CliCommandBinding;
|
|
|
|
typedef struct EmbeddedCli EmbeddedCli;
|
|
|
|
typedef struct EmbeddedCliConfig EmbeddedCliConfig;
|
|
|
|
|
|
|
|
|
|
|
|
struct CliCommand {
|
|
|
|
/**
|
|
|
|
* Name of the command.
|
|
|
|
* In command "set led 1 1" "set" is name
|
|
|
|
*/
|
|
|
|
const char *name;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* String of arguments of the command.
|
|
|
|
* In command "set led 1 1" "led 1 1" is string of arguments
|
|
|
|
* Is ended with double 0x00 char
|
|
|
|
* Use tokenize functions to easily get individual tokens
|
|
|
|
*/
|
|
|
|
char *args;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Struct to describe binding of command to function and
|
|
|
|
*/
|
|
|
|
struct CliCommandBinding {
|
|
|
|
/**
|
|
|
|
* Name of command to bind. Should not be NULL.
|
|
|
|
*/
|
|
|
|
const char *name;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Help string that will be displayed when "help <cmd>" is executed.
|
|
|
|
* Can have multiple lines separated with "\r\n"
|
|
|
|
* Can be NULL if no help is provided.
|
|
|
|
*/
|
|
|
|
const char *help;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flag to perform tokenization before calling binding function.
|
|
|
|
*/
|
|
|
|
bool tokenizeArgs;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pointer to any specific app context that is required for this binding.
|
|
|
|
* It will be provided in binding callback.
|
|
|
|
*/
|
|
|
|
void *context;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binding function for when command is received.
|
|
|
|
* If null, default callback (onCommand) will be called.
|
|
|
|
* @param cli - pointer to cli that is calling this binding
|
|
|
|
* @param args - string of args (if tokenizeArgs is false) or tokens otherwise
|
|
|
|
* @param context
|
|
|
|
*/
|
|
|
|
void (*binding)(EmbeddedCli *cli, char *args, void *context);
|
|
|
|
};
|
|
|
|
|
|
|
|
struct EmbeddedCli {
|
|
|
|
/**
|
|
|
|
* Should write char to connection
|
|
|
|
* @param cli - pointer to cli that executed this function
|
|
|
|
* @param c - actual character to write
|
|
|
|
*/
|
|
|
|
void (*writeChar)(EmbeddedCli *cli, char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when command is received and command not found in list of
|
|
|
|
* command bindings (or binding function is null).
|
|
|
|
* @param cli - pointer to cli that executed this function
|
|
|
|
* @param command - pointer to received command
|
|
|
|
*/
|
|
|
|
void (*onCommand)(EmbeddedCli *cli, CliCommand *command);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can be used by for any application context
|
|
|
|
*/
|
|
|
|
void *appContext;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pointer to actual implementation, do not use.
|
|
|
|
*/
|
|
|
|
void *_impl;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configuration to create CLI
|
|
|
|
*/
|
|
|
|
struct EmbeddedCliConfig {
|
|
|
|
/**
|
|
|
|
* Size of buffer that is used to store characters until they're processed
|
|
|
|
*/
|
|
|
|
uint16_t rxBufferSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Size of buffer that is used to store current input that is not yet
|
|
|
|
* sended as command (return not pressed yet)
|
|
|
|
*/
|
|
|
|
uint16_t cmdBufferSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Size of buffer that is used to store previously entered commands
|
|
|
|
* Only unique commands are stored in buffer. If buffer is smaller than
|
|
|
|
* entered command (including arguments), command is discarded from history
|
|
|
|
*/
|
|
|
|
uint16_t historyBufferSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum amount of bindings that can be added via addBinding function.
|
|
|
|
* Cli increases takes extra bindings for internal commands:
|
|
|
|
* - help
|
|
|
|
*/
|
|
|
|
uint16_t maxBindingCount;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Buffer to use for cli and all internal structures. If NULL, memory will
|
|
|
|
* be allocated dynamically. Otherwise this buffer is used and no
|
|
|
|
* allocations are made
|
|
|
|
*/
|
|
|
|
CLI_UINT *cliBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Size of buffer for cli and internal structures (in bytes).
|
|
|
|
*/
|
|
|
|
uint16_t cliBufferSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether autocompletion should be enabled.
|
|
|
|
* If false, autocompletion is disabled but you still can use 'tab' to
|
|
|
|
* complete current command manually.
|
|
|
|
*/
|
|
|
|
bool enableAutoComplete;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns pointer to default configuration for cli creation. It is safe to
|
|
|
|
* modify it and then send to embeddedCliNew().
|
|
|
|
* Returned structure is always the same so do not free and try to use it
|
|
|
|
* immediately.
|
|
|
|
* Default values:
|
|
|
|
* <ul>
|
|
|
|
* <li>rxBufferSize = 64</li>
|
|
|
|
* <li>cmdBufferSize = 64</li>
|
|
|
|
* <li>historyBufferSize = 128</li>
|
|
|
|
* <li>cliBuffer = NULL (use dynamic allocation)</li>
|
|
|
|
* <li>cliBufferSize = 0</li>
|
|
|
|
* <li>maxBindingCount = 8</li>
|
|
|
|
* <li>enableAutoComplete = true</li>
|
|
|
|
* </ul>
|
|
|
|
* @return configuration for cli creation
|
|
|
|
*/
|
|
|
|
EmbeddedCliConfig *embeddedCliDefaultConfig(void);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns how many space in config buffer is required for cli creation
|
|
|
|
* If you provide buffer with less space, embeddedCliNew will return NULL
|
|
|
|
* This amount will always be divisible by CLI_UINT_SIZE so allocated buffer
|
|
|
|
* and internal structures can be properly aligned
|
|
|
|
* @param config
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create new CLI.
|
|
|
|
* Memory is allocated dynamically if cliBuffer in config is NULL.
|
|
|
|
* After CLI is created, override function pointers to start using it
|
|
|
|
* @param config - config for cli creation
|
|
|
|
* @return pointer to created CLI
|
|
|
|
*/
|
|
|
|
EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Same as calling embeddedCliNew with default config.
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
EmbeddedCli *embeddedCliNewDefault(void);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Receive character and put it to internal buffer
|
|
|
|
* Actual processing is done inside embeddedCliProcess
|
|
|
|
* You can call this function from something like interrupt service routine,
|
|
|
|
* just make sure that you call it only from single place. Otherwise input
|
|
|
|
* might get corrupted
|
|
|
|
* @param cli
|
|
|
|
* @param c - received char
|
|
|
|
*/
|
|
|
|
void embeddedCliReceiveChar(EmbeddedCli *cli, char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process rx/tx buffers. Command callbacks are called from here
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
void embeddedCliProcess(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add specified binding to list of bindings. If list is already full, binding
|
|
|
|
* is not added and false is returned
|
|
|
|
* @param cli
|
|
|
|
* @param binding
|
|
|
|
* @return true if binding was added, false otherwise
|
|
|
|
*/
|
|
|
|
bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Print specified string and account for currently entered but not submitted
|
|
|
|
* command.
|
|
|
|
* Current command is deleted, provided string is printed (with new line) after
|
|
|
|
* that current command is printed again, so user can continue typing it.
|
|
|
|
* @param cli
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
void embeddedCliPrint(EmbeddedCli *cli, const char *string);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Free allocated for cli memory
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
void embeddedCliFree(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform tokenization of arguments string. Original string is modified and
|
|
|
|
* should not be used directly (only inside other token functions).
|
|
|
|
* Individual tokens are separated by single 0x00 char, double 0x00 is put at
|
|
|
|
* the end of token list. After calling this function, you can use other
|
|
|
|
* token functions to get individual tokens and token count.
|
|
|
|
*
|
|
|
|
* Important: Call this function only once. Otherwise information will be lost if
|
|
|
|
* more than one token existed
|
|
|
|
* @param args - string to tokenize (must have extra writable char after 0x00)
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
void embeddedCliTokenizeArgs(char *args);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return specific token from tokenized string
|
|
|
|
* @param tokenizedStr
|
|
|
|
* @param pos (counted from 1)
|
|
|
|
* @return token
|
|
|
|
*/
|
|
|
|
const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Same as embeddedCliGetToken but works on non-const buffer
|
|
|
|
* @param tokenizedStr
|
|
|
|
* @param pos (counted from 1)
|
|
|
|
* @return token
|
|
|
|
*/
|
|
|
|
char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find token in provided tokens string and return its position (counted from 1)
|
|
|
|
* If no such token is found - 0 is returned.
|
|
|
|
* @param tokenizedStr
|
|
|
|
* @param token - token to find
|
|
|
|
* @return position (increased by 1) or zero if no such token found
|
|
|
|
*/
|
|
|
|
uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return number of tokens in tokenized string
|
|
|
|
* @param tokenizedStr
|
|
|
|
* @return number of tokens
|
|
|
|
*/
|
|
|
|
uint16_t embeddedCliGetTokenCount(const char *tokenizedStr);
|
|
|
|
|
|
|
|
#ifdef __cplusplus
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#endif //EMBEDDED_CLI_H
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef EMBEDDED_CLI_IMPL
|
|
|
|
#ifndef EMBEDDED_CLI_IMPL_GUARD
|
|
|
|
#define EMBEDDED_CLI_IMPL_GUARD
|
|
|
|
#ifdef __cplusplus
|
|
|
|
extern "C" {
|
|
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
|
|
#define CLI_TOKEN_NPOS 0xffff
|
|
|
|
|
|
|
|
#define UNUSED(x) (void)x
|
|
|
|
|
|
|
|
#define PREPARE_IMPL(t) \
|
|
|
|
EmbeddedCliImpl* impl = (EmbeddedCliImpl*)t->_impl
|
|
|
|
|
|
|
|
#define IS_FLAG_SET(flags, flag) (((flags) & (flag)) != 0)
|
|
|
|
|
|
|
|
#define SET_FLAG(flags, flag) ((flags) |= (flag))
|
|
|
|
|
|
|
|
#define UNSET_U8FLAG(flags, flag) ((flags) &= (uint8_t) ~(flag))
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Marks binding as candidate for autocompletion
|
|
|
|
* This flag is updated each time getAutocompletedCommand is called
|
|
|
|
*/
|
|
|
|
#define BINDING_FLAG_AUTOCOMPLETE 1u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that rx buffer overflow happened. In such case last command
|
|
|
|
* that wasn't finished (no \r or \n were received) will be discarded
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_OVERFLOW 0x01u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that initialization is completed. Initialization is completed in
|
|
|
|
* first call to process and needed, for example, to print invitation message.
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_INIT_COMPLETE 0x02u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that CLI structure and internal structures were allocated with
|
|
|
|
* malloc and should bre freed
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_ALLOCATED 0x04u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that CLI structure and internal structures were allocated with
|
|
|
|
* malloc and should bre freed
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_ESCAPE_MODE 0x08u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that CLI in mode when it will print directly to output without
|
|
|
|
* clear of current command and printing it back
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_DIRECT_PRINT 0x10u
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Indicates that live autocompletion is enabled
|
|
|
|
*/
|
|
|
|
#define CLI_FLAG_AUTOCOMPLETE_ENABLED 0x20u
|
|
|
|
|
|
|
|
typedef struct EmbeddedCliImpl EmbeddedCliImpl;
|
|
|
|
typedef struct AutocompletedCommand AutocompletedCommand;
|
|
|
|
typedef struct FifoBuf FifoBuf;
|
|
|
|
typedef struct CliHistory CliHistory;
|
|
|
|
|
|
|
|
struct FifoBuf {
|
|
|
|
char *buf;
|
|
|
|
/**
|
|
|
|
* Position of first element in buffer. From this position elements are taken
|
|
|
|
*/
|
|
|
|
uint16_t front;
|
|
|
|
/**
|
|
|
|
* Position after last element. At this position new elements are inserted
|
|
|
|
*/
|
|
|
|
uint16_t back;
|
|
|
|
/**
|
|
|
|
* Size of buffer
|
|
|
|
*/
|
|
|
|
uint16_t size;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct CliHistory {
|
|
|
|
/**
|
|
|
|
* Items in buffer are separated by null-chars
|
|
|
|
*/
|
|
|
|
char *buf;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total size of buffer
|
|
|
|
*/
|
|
|
|
uint16_t bufferSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Index of currently selected element. This allows to navigate history
|
|
|
|
* After command is sent, current element is reset to 0 (no element)
|
|
|
|
*/
|
|
|
|
uint16_t current;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of items in buffer
|
|
|
|
* Items are counted from top to bottom (and are 1 based).
|
|
|
|
* So the most recent item is 1 and the oldest is itemCount.
|
|
|
|
*/
|
|
|
|
uint16_t itemsCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct EmbeddedCliImpl {
|
|
|
|
/**
|
|
|
|
* Invitation string. Is printed at the beginning of each line with user
|
|
|
|
* input
|
|
|
|
*/
|
|
|
|
const char *invitation;
|
|
|
|
|
|
|
|
CliHistory history;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Buffer for storing received chars.
|
|
|
|
* Chars are stored in FIFO mode.
|
|
|
|
*/
|
|
|
|
FifoBuf rxBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Buffer for current command
|
|
|
|
*/
|
|
|
|
char *cmdBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Size of current command
|
|
|
|
*/
|
|
|
|
uint16_t cmdSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total size of command buffer
|
|
|
|
*/
|
|
|
|
uint16_t cmdMaxSize;
|
|
|
|
|
|
|
|
CliCommandBinding *bindings;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flags for each binding. Sizes are the same as for bindings array
|
|
|
|
*/
|
|
|
|
uint8_t *bindingsFlags;
|
|
|
|
|
|
|
|
uint16_t bindingsCount;
|
|
|
|
|
|
|
|
uint16_t maxBindingsCount;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total length of input line. This doesn't include invitation but
|
|
|
|
* includes current command and its live autocompletion
|
|
|
|
*/
|
|
|
|
uint16_t inputLineLength;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stores last character that was processed.
|
|
|
|
*/
|
|
|
|
char lastChar;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flags are defined as CLI_FLAG_*
|
|
|
|
*/
|
|
|
|
uint8_t flags;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct AutocompletedCommand {
|
|
|
|
/**
|
|
|
|
* Name of autocompleted command (or first candidate for autocompletion if
|
|
|
|
* there are multiple candidates).
|
|
|
|
* NULL if autocomplete not possible.
|
|
|
|
*/
|
|
|
|
const char *firstCandidate;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of characters that can be completed safely. For example, if there
|
|
|
|
* are two possible commands "get-led" and "get-adc", then for prefix "g"
|
|
|
|
* autocompletedLen will be 4. If there are only one candidate, this number
|
|
|
|
* is always equal to length of the command.
|
|
|
|
*/
|
|
|
|
uint16_t autocompletedLen;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total number of candidates for autocompletion
|
|
|
|
*/
|
|
|
|
uint16_t candidateCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
static EmbeddedCliConfig defaultConfig;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of commands that cli adds. Commands:
|
|
|
|
* - help
|
|
|
|
*/
|
|
|
|
static const uint16_t cliInternalBindingCount = 1;
|
|
|
|
|
|
|
|
static const char *lineBreak = "\r\n";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Navigate through command history back and forth. If navigateUp is true,
|
|
|
|
* navigate to older commands, otherwise navigate to newer.
|
|
|
|
* When history end is reached, nothing happens.
|
|
|
|
* @param cli
|
|
|
|
* @param navigateUp
|
|
|
|
*/
|
|
|
|
static void navigateHistory(EmbeddedCli *cli, bool navigateUp);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process escaped character. After receiving ESC+[ sequence, all chars up to
|
|
|
|
* ending character are sent to this function
|
|
|
|
* @param cli
|
|
|
|
* @param c
|
|
|
|
*/
|
|
|
|
static void onEscapedInput(EmbeddedCli *cli, char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process input character. Character is valid displayable char and should be
|
|
|
|
* added to current command string and displayed to client.
|
|
|
|
* @param cli
|
|
|
|
* @param c
|
|
|
|
*/
|
|
|
|
static void onCharInput(EmbeddedCli *cli, char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process control character (like \r or \n) possibly altering state of current
|
|
|
|
* command or executing onCommand callback.
|
|
|
|
* @param cli
|
|
|
|
* @param c
|
|
|
|
*/
|
|
|
|
static void onControlInput(EmbeddedCli *cli, char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse command in buffer and execute callback
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
static void parseCommand(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Setup bindings for internal commands, like help
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
static void initInternalBindings(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show help for given tokens (or default help if no tokens)
|
|
|
|
* @param cli
|
|
|
|
* @param tokens
|
|
|
|
* @param context - not used
|
|
|
|
*/
|
|
|
|
static void onHelp(EmbeddedCli *cli, char *tokens, void *context);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show error about unknown command
|
|
|
|
* @param cli
|
|
|
|
* @param name
|
|
|
|
*/
|
|
|
|
static void onUnknownCommand(EmbeddedCli *cli, const char *name);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return autocompleted command for given prefix.
|
|
|
|
* Prefix is compared to all known command bindings and autocompleted result
|
|
|
|
* is returned
|
|
|
|
* @param cli
|
|
|
|
* @param prefix
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prints autocompletion result while keeping current command unchanged
|
|
|
|
* Prints only if autocompletion is present and only one candidate exists.
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
static void printLiveAutocompletion(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles autocomplete request. If autocomplete possible - fills current
|
|
|
|
* command with autocompleted command. When multiple commands satisfy entered
|
|
|
|
* prefix, they are printed to output.
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
static void onAutocompleteRequest(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes all input from current line (replaces it with whitespaces)
|
|
|
|
* And places cursor at the beginning of the line
|
|
|
|
* @param cli
|
|
|
|
*/
|
|
|
|
static void clearCurrentLine(EmbeddedCli *cli);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Write given string to cli output
|
|
|
|
* @param cli
|
|
|
|
* @param str
|
|
|
|
*/
|
|
|
|
static void writeToOutput(EmbeddedCli *cli, const char *str);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if provided char is a supported control char:
|
|
|
|
* \r, \n, \b or 0x7F (treated as \b)
|
|
|
|
* @param c
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
static bool isControlChar(char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if provided char is a valid displayable character:
|
|
|
|
* a-z, A-Z, 0-9, whitespace, punctuation, etc.
|
|
|
|
* Currently only ASCII is supported
|
|
|
|
* @param c
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
static bool isDisplayableChar(char c);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* How many elements are currently available in buffer
|
|
|
|
* @param buffer
|
|
|
|
* @return number of elements
|
|
|
|
*/
|
|
|
|
static uint16_t fifoBufAvailable(FifoBuf *buffer);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return first character from buffer and remove it from buffer
|
|
|
|
* Buffer must be non-empty, otherwise 0 is returned
|
|
|
|
* @param buffer
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
static char fifoBufPop(FifoBuf *buffer);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Push character into fifo buffer. If there is no space left, character is
|
|
|
|
* discarded and false is returned
|
|
|
|
* @param buffer
|
|
|
|
* @param a - character to add
|
|
|
|
* @return true if char was added to buffer, false otherwise
|
|
|
|
*/
|
|
|
|
static bool fifoBufPush(FifoBuf *buffer, char a);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Copy provided string to the history buffer.
|
|
|
|
* If it is already inside history, it will be removed from it and added again.
|
|
|
|
* So after addition, it will always be on top
|
|
|
|
* If available size is not enough (and total size is enough) old elements will
|
|
|
|
* be removed from history so this item can be put to it
|
|
|
|
* @param history
|
|
|
|
* @param str
|
|
|
|
* @return true if string was put in history
|
|
|
|
*/
|
|
|
|
static bool historyPut(CliHistory *history, const char *str);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get item from history. Items are counted from 1 so if item is 0 or greater
|
|
|
|
* than itemCount, NULL is returned
|
|
|
|
* @param history
|
|
|
|
* @param item
|
|
|
|
* @return true if string was put in history
|
|
|
|
*/
|
|
|
|
static const char *historyGet(CliHistory *history, uint16_t item);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove specific item from history
|
|
|
|
* @param history
|
|
|
|
* @param str - string to remove
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
static void historyRemove(CliHistory *history, const char *str);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return position (index of first char) of specified token
|
|
|
|
* @param tokenizedStr - tokenized string (separated by \0 with
|
|
|
|
* \0\0 at the end)
|
|
|
|
* @param pos - token position (counted from 1)
|
|
|
|
* @return index of first char of specified token
|
|
|
|
*/
|
|
|
|
static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos);
|
|
|
|
|
|
|
|
EmbeddedCliConfig *embeddedCliDefaultConfig(void) {
|
|
|
|
defaultConfig.rxBufferSize = 64;
|
|
|
|
defaultConfig.cmdBufferSize = 64;
|
|
|
|
defaultConfig.historyBufferSize = 128;
|
|
|
|
defaultConfig.cliBuffer = NULL;
|
|
|
|
defaultConfig.cliBufferSize = 0;
|
|
|
|
defaultConfig.maxBindingCount = 8;
|
|
|
|
defaultConfig.enableAutoComplete = true;
|
|
|
|
return &defaultConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config) {
|
|
|
|
uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount);
|
|
|
|
return (uint16_t) (CLI_UINT_SIZE * (
|
|
|
|
BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli)) +
|
|
|
|
BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl)) +
|
|
|
|
BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char)) +
|
|
|
|
BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char)) +
|
|
|
|
BYTES_TO_CLI_UINTS(config->historyBufferSize * sizeof(char)) +
|
|
|
|
BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding)) +
|
|
|
|
BYTES_TO_CLI_UINTS(bindingCount * sizeof(uint8_t))));
|
|
|
|
}
|
|
|
|
|
|
|
|
EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config) {
|
|
|
|
EmbeddedCli *cli = NULL;
|
|
|
|
|
|
|
|
uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount);
|
|
|
|
|
|
|
|
size_t totalSize = embeddedCliRequiredSize(config);
|
|
|
|
|
|
|
|
bool allocated = false;
|
|
|
|
if (config->cliBuffer == NULL) {
|
2024-04-22 22:33:39 +07:00
|
|
|
// config->cliBuffer = (CLI_UINT *) malloc(totalSize); // malloc guarantees alignment.
|
2022-11-18 21:38:03 +07:00
|
|
|
if (config->cliBuffer == NULL)
|
|
|
|
return NULL;
|
|
|
|
allocated = true;
|
|
|
|
} else if (config->cliBufferSize < totalSize) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
CLI_UINT *buf = config->cliBuffer;
|
|
|
|
|
|
|
|
memset(buf, 0, totalSize);
|
|
|
|
|
|
|
|
cli = (EmbeddedCli *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli));
|
|
|
|
|
|
|
|
cli->_impl = (EmbeddedCliImpl *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl));
|
|
|
|
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
impl->rxBuffer.buf = (char *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char));
|
|
|
|
|
|
|
|
impl->cmdBuffer = (char *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char));
|
|
|
|
|
|
|
|
impl->bindings = (CliCommandBinding *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding));
|
|
|
|
|
|
|
|
impl->bindingsFlags = (uint8_t *) buf;
|
|
|
|
buf += BYTES_TO_CLI_UINTS(bindingCount);
|
|
|
|
|
|
|
|
impl->history.buf = (char *) buf;
|
|
|
|
impl->history.bufferSize = config->historyBufferSize;
|
|
|
|
|
|
|
|
if (allocated)
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_ALLOCATED);
|
|
|
|
|
|
|
|
if (config->enableAutoComplete)
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED);
|
|
|
|
|
|
|
|
impl->rxBuffer.size = config->rxBufferSize;
|
|
|
|
impl->rxBuffer.front = 0;
|
|
|
|
impl->rxBuffer.back = 0;
|
|
|
|
impl->cmdMaxSize = config->cmdBufferSize;
|
|
|
|
impl->bindingsCount = 0;
|
|
|
|
impl->maxBindingsCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount);
|
|
|
|
impl->lastChar = '\0';
|
|
|
|
impl->invitation = "> ";
|
|
|
|
|
|
|
|
initInternalBindings(cli);
|
|
|
|
|
|
|
|
return cli;
|
|
|
|
}
|
|
|
|
|
|
|
|
EmbeddedCli *embeddedCliNewDefault(void) {
|
|
|
|
return embeddedCliNew(embeddedCliDefaultConfig());
|
|
|
|
}
|
|
|
|
|
|
|
|
void embeddedCliReceiveChar(EmbeddedCli *cli, char c) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
if (!fifoBufPush(&impl->rxBuffer, c)) {
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_OVERFLOW);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void embeddedCliProcess(EmbeddedCli *cli) {
|
|
|
|
if (cli->writeChar == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
|
|
|
|
if (!IS_FLAG_SET(impl->flags, CLI_FLAG_INIT_COMPLETE)) {
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_INIT_COMPLETE);
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
}
|
|
|
|
|
|
|
|
while (fifoBufAvailable(&impl->rxBuffer)) {
|
|
|
|
char c = fifoBufPop(&impl->rxBuffer);
|
|
|
|
|
|
|
|
if (IS_FLAG_SET(impl->flags, CLI_FLAG_ESCAPE_MODE)) {
|
|
|
|
onEscapedInput(cli, c);
|
|
|
|
} else if (impl->lastChar == 0x1B && c == '[') {
|
|
|
|
//enter escape mode
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE);
|
|
|
|
} else if (isControlChar(c)) {
|
|
|
|
onControlInput(cli, c);
|
|
|
|
} else if (isDisplayableChar(c)) {
|
|
|
|
onCharInput(cli, c);
|
|
|
|
}
|
|
|
|
|
|
|
|
printLiveAutocompletion(cli);
|
|
|
|
|
|
|
|
impl->lastChar = c;
|
|
|
|
}
|
|
|
|
|
|
|
|
// discard unfinished command if overflow happened
|
|
|
|
if (IS_FLAG_SET(impl->flags, CLI_FLAG_OVERFLOW)) {
|
|
|
|
impl->cmdSize = 0;
|
|
|
|
impl->cmdBuffer[impl->cmdSize] = '\0';
|
|
|
|
UNSET_U8FLAG(impl->flags, CLI_FLAG_OVERFLOW);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
if (impl->bindingsCount == impl->maxBindingsCount)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
impl->bindings[impl->bindingsCount] = binding;
|
|
|
|
|
|
|
|
++impl->bindingsCount;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void embeddedCliPrint(EmbeddedCli *cli, const char *string) {
|
|
|
|
if (cli->writeChar == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
// remove chars for autocompletion and live command
|
|
|
|
if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT))
|
|
|
|
clearCurrentLine(cli);
|
|
|
|
|
|
|
|
// print provided string
|
|
|
|
writeToOutput(cli, string);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
|
|
|
|
// print current command back to screen
|
|
|
|
if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) {
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
writeToOutput(cli, impl->cmdBuffer);
|
|
|
|
impl->inputLineLength = impl->cmdSize;
|
|
|
|
|
|
|
|
printLiveAutocompletion(cli);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void embeddedCliFree(EmbeddedCli *cli) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
if (IS_FLAG_SET(impl->flags, CLI_FLAG_ALLOCATED)) {
|
|
|
|
// allocation is done in single call to malloc, so need only single free
|
2024-04-22 22:33:39 +07:00
|
|
|
// free(cli);
|
2022-11-18 21:38:03 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void embeddedCliTokenizeArgs(char *args) {
|
|
|
|
if (args == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// for now only space, but can add more later
|
|
|
|
const char *separators = " ";
|
|
|
|
|
|
|
|
// indicates that arg is quoted so separators are copied as is
|
|
|
|
bool quotesEnabled = false;
|
|
|
|
// indicates that previous char was a slash, so next char is copied as is
|
|
|
|
bool escapeActivated = false;
|
|
|
|
int insertPos = 0;
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
char currentChar;
|
|
|
|
while ((currentChar = args[i]) != '\0') {
|
|
|
|
++i;
|
|
|
|
|
|
|
|
if (escapeActivated) {
|
|
|
|
escapeActivated = false;
|
|
|
|
} else if (currentChar == '\\') {
|
|
|
|
escapeActivated = true;
|
|
|
|
continue;
|
|
|
|
} else if (currentChar == '"') {
|
|
|
|
quotesEnabled = !quotesEnabled;
|
|
|
|
currentChar = '\0';
|
|
|
|
} else if (!quotesEnabled && strchr(separators, currentChar) != NULL) {
|
|
|
|
currentChar = '\0';
|
|
|
|
}
|
|
|
|
|
|
|
|
// null chars are only copied once and not copied to the beginning
|
|
|
|
if (currentChar != '\0' || (insertPos > 0 && args[insertPos - 1] != '\0')) {
|
|
|
|
args[insertPos] = currentChar;
|
|
|
|
++insertPos;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// make args double null-terminated source buffer must be big enough to contain extra spaces
|
|
|
|
args[insertPos] = '\0';
|
|
|
|
args[insertPos + 1] = '\0';
|
|
|
|
}
|
|
|
|
|
|
|
|
const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos) {
|
|
|
|
uint16_t i = getTokenPosition(tokenizedStr, pos);
|
|
|
|
|
|
|
|
if (i != CLI_TOKEN_NPOS)
|
|
|
|
return &tokenizedStr[i];
|
|
|
|
else
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos) {
|
|
|
|
uint16_t i = getTokenPosition(tokenizedStr, pos);
|
|
|
|
|
|
|
|
if (i != CLI_TOKEN_NPOS)
|
|
|
|
return &tokenizedStr[i];
|
|
|
|
else
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token) {
|
|
|
|
if (tokenizedStr == NULL || token == NULL)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
uint16_t size = embeddedCliGetTokenCount(tokenizedStr);
|
|
|
|
for (uint16_t i = 1; i <= size; ++i) {
|
|
|
|
if (strcmp(embeddedCliGetToken(tokenizedStr, i), token) == 0)
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t embeddedCliGetTokenCount(const char *tokenizedStr) {
|
|
|
|
if (tokenizedStr == NULL || tokenizedStr[0] == '\0')
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
uint16_t tokenCount = 1;
|
|
|
|
while (true) {
|
|
|
|
if (tokenizedStr[i] == '\0') {
|
|
|
|
if (tokenizedStr[i + 1] == '\0')
|
|
|
|
break;
|
|
|
|
++tokenCount;
|
|
|
|
}
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
|
|
|
return tokenCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void navigateHistory(EmbeddedCli *cli, bool navigateUp) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
if (impl->history.itemsCount == 0 ||
|
|
|
|
(navigateUp && impl->history.current == impl->history.itemsCount) ||
|
|
|
|
(!navigateUp && impl->history.current == 0))
|
|
|
|
return;
|
|
|
|
|
|
|
|
clearCurrentLine(cli);
|
|
|
|
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
|
|
|
|
if (navigateUp)
|
|
|
|
++impl->history.current;
|
|
|
|
else
|
|
|
|
--impl->history.current;
|
|
|
|
|
|
|
|
const char *item = historyGet(&impl->history, impl->history.current);
|
|
|
|
// simple way to handle empty command the same way as others
|
|
|
|
if (item == NULL)
|
|
|
|
item = "";
|
|
|
|
uint16_t len = (uint16_t) strlen(item);
|
|
|
|
memcpy(impl->cmdBuffer, item, len);
|
|
|
|
impl->cmdBuffer[len] = '\0';
|
|
|
|
impl->cmdSize = len;
|
|
|
|
|
|
|
|
writeToOutput(cli, impl->cmdBuffer);
|
|
|
|
impl->inputLineLength = impl->cmdSize;
|
|
|
|
|
|
|
|
printLiveAutocompletion(cli);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onEscapedInput(EmbeddedCli *cli, char c) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
if (c >= 64 && c <= 126) {
|
|
|
|
// handle escape sequence
|
|
|
|
UNSET_U8FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE);
|
|
|
|
|
|
|
|
if (c == 'A' || c == 'B') {
|
|
|
|
// treat \e[..A as cursor up and \e[..B as cursor down
|
|
|
|
// there might be extra chars between [ and A/B, just ignore them
|
|
|
|
navigateHistory(cli, c == 'A');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onCharInput(EmbeddedCli *cli, char c) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
// have to reserve two extra chars for command ending (used in tokenization)
|
|
|
|
if (impl->cmdSize + 2 >= impl->cmdMaxSize)
|
|
|
|
return;
|
|
|
|
|
|
|
|
impl->cmdBuffer[impl->cmdSize] = c;
|
|
|
|
++impl->cmdSize;
|
|
|
|
impl->cmdBuffer[impl->cmdSize] = '\0';
|
|
|
|
|
|
|
|
cli->writeChar(cli, c);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onControlInput(EmbeddedCli *cli, char c) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
// process \r\n and \n\r as single \r\n command
|
|
|
|
if ((impl->lastChar == '\r' && c == '\n') ||
|
|
|
|
(impl->lastChar == '\n' && c == '\r'))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (c == '\r' || c == '\n') {
|
|
|
|
// try to autocomplete command and then process it
|
|
|
|
onAutocompleteRequest(cli);
|
|
|
|
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
|
|
|
|
if (impl->cmdSize > 0)
|
|
|
|
parseCommand(cli);
|
|
|
|
impl->cmdSize = 0;
|
|
|
|
impl->cmdBuffer[impl->cmdSize] = '\0';
|
|
|
|
impl->inputLineLength = 0;
|
|
|
|
impl->history.current = 0;
|
|
|
|
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
} else if ((c == '\b' || c == 0x7F) && impl->cmdSize > 0) {
|
|
|
|
// remove char from screen
|
|
|
|
cli->writeChar(cli, '\b');
|
|
|
|
cli->writeChar(cli, ' ');
|
|
|
|
cli->writeChar(cli, '\b');
|
|
|
|
// and from buffer
|
|
|
|
--impl->cmdSize;
|
|
|
|
impl->cmdBuffer[impl->cmdSize] = '\0';
|
|
|
|
} else if (c == '\t') {
|
|
|
|
onAutocompleteRequest(cli);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
static void parseCommand(EmbeddedCli *cli) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
bool isEmpty = true;
|
|
|
|
|
|
|
|
for (int i = 0; i < impl->cmdSize; ++i) {
|
|
|
|
if (impl->cmdBuffer[i] != ' ') {
|
|
|
|
isEmpty = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// do not process empty commands
|
|
|
|
if (isEmpty)
|
|
|
|
return;
|
|
|
|
// push command to history before buffer is modified
|
|
|
|
historyPut(&impl->history, impl->cmdBuffer);
|
|
|
|
|
|
|
|
char *cmdName = NULL;
|
|
|
|
char *cmdArgs = NULL;
|
|
|
|
bool nameFinished = false;
|
|
|
|
|
|
|
|
// find command name and command args inside command buffer
|
|
|
|
for (int i = 0; i < impl->cmdSize; ++i) {
|
|
|
|
char c = impl->cmdBuffer[i];
|
|
|
|
|
|
|
|
if (c == ' ') {
|
|
|
|
// all spaces between name and args are filled with zeros
|
|
|
|
// so name is a correct null-terminated string
|
|
|
|
if (cmdArgs == NULL)
|
|
|
|
impl->cmdBuffer[i] = '\0';
|
|
|
|
if (cmdName != NULL)
|
|
|
|
nameFinished = true;
|
|
|
|
|
|
|
|
} else if (cmdName == NULL) {
|
|
|
|
cmdName = &impl->cmdBuffer[i];
|
|
|
|
} else if (cmdArgs == NULL && nameFinished) {
|
|
|
|
cmdArgs = &impl->cmdBuffer[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// we keep two last bytes in cmd buffer reserved so cmdSize is always by 2
|
|
|
|
// less than cmdMaxSize
|
|
|
|
impl->cmdBuffer[impl->cmdSize + 1] = '\0';
|
|
|
|
|
|
|
|
if (cmdName == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// try to find command in bindings
|
|
|
|
for (int i = 0; i < impl->bindingsCount; ++i) {
|
|
|
|
if (strcmp(cmdName, impl->bindings[i].name) == 0) {
|
|
|
|
if (impl->bindings[i].binding == NULL)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (impl->bindings[i].tokenizeArgs)
|
|
|
|
embeddedCliTokenizeArgs(cmdArgs);
|
|
|
|
// currently, output is blank line, so we can just print directly
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT);
|
|
|
|
impl->bindings[i].binding(cli, cmdArgs, impl->bindings[i].context);
|
|
|
|
UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// command not found in bindings or binding was null
|
|
|
|
// try to call default callback
|
|
|
|
if (cli->onCommand != NULL) {
|
|
|
|
CliCommand command;
|
|
|
|
command.name = cmdName;
|
|
|
|
command.args = cmdArgs;
|
|
|
|
|
|
|
|
// currently, output is blank line, so we can just print directly
|
|
|
|
SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT);
|
|
|
|
cli->onCommand(cli, &command);
|
|
|
|
UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT);
|
|
|
|
} else {
|
|
|
|
onUnknownCommand(cli, cmdName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void initInternalBindings(EmbeddedCli *cli) {
|
|
|
|
CliCommandBinding b = {
|
|
|
|
"help",
|
|
|
|
"Print list of commands",
|
|
|
|
true,
|
|
|
|
NULL,
|
|
|
|
onHelp
|
|
|
|
};
|
|
|
|
embeddedCliAddBinding(cli, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onHelp(EmbeddedCli *cli, char *tokens, void *context) {
|
|
|
|
UNUSED(context);
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
if (impl->bindingsCount == 0) {
|
|
|
|
writeToOutput(cli, "Help is not available");
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t tokenCount = embeddedCliGetTokenCount(tokens);
|
|
|
|
if (tokenCount == 0) {
|
|
|
|
for (int i = 0; i < impl->bindingsCount; ++i) {
|
|
|
|
writeToOutput(cli, " * ");
|
|
|
|
writeToOutput(cli, impl->bindings[i].name);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
if (impl->bindings[i].help != NULL) {
|
|
|
|
cli->writeChar(cli, '\t');
|
|
|
|
writeToOutput(cli, impl->bindings[i].help);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (tokenCount == 1) {
|
|
|
|
// try find command
|
|
|
|
const char *helpStr = NULL;
|
|
|
|
const char *cmdName = embeddedCliGetToken(tokens, 1);
|
|
|
|
bool found = false;
|
|
|
|
for (int i = 0; i < impl->bindingsCount; ++i) {
|
|
|
|
if (strcmp(impl->bindings[i].name, cmdName) == 0) {
|
|
|
|
helpStr = impl->bindings[i].help;
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (found && helpStr != NULL) {
|
|
|
|
writeToOutput(cli, " * ");
|
|
|
|
writeToOutput(cli, cmdName);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
cli->writeChar(cli, '\t');
|
|
|
|
writeToOutput(cli, helpStr);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
} else if (found) {
|
|
|
|
writeToOutput(cli, "Help is not available");
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
} else {
|
|
|
|
onUnknownCommand(cli, cmdName);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
writeToOutput(cli, "Command \"help\" receives one or zero arguments");
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onUnknownCommand(EmbeddedCli *cli, const char *name) {
|
|
|
|
writeToOutput(cli, "Unknown command: \"");
|
|
|
|
writeToOutput(cli, name);
|
|
|
|
writeToOutput(cli, "\". Write \"help\" for a list of available commands");
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
}
|
|
|
|
|
|
|
|
static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix) {
|
|
|
|
AutocompletedCommand cmd = {NULL, 0, 0};
|
|
|
|
|
|
|
|
size_t prefixLen = strlen(prefix);
|
|
|
|
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
if (impl->bindingsCount == 0 || prefixLen == 0)
|
|
|
|
return cmd;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < impl->bindingsCount; ++i) {
|
|
|
|
const char *name = impl->bindings[i].name;
|
|
|
|
size_t len = strlen(name);
|
|
|
|
|
|
|
|
// unset autocomplete flag
|
|
|
|
UNSET_U8FLAG(impl->bindingsFlags[i], BINDING_FLAG_AUTOCOMPLETE);
|
|
|
|
|
|
|
|
if (len < prefixLen)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// check if this command is candidate for autocomplete
|
|
|
|
bool isCandidate = true;
|
|
|
|
for (size_t j = 0; j < prefixLen; ++j) {
|
|
|
|
if (prefix[j] != name[j]) {
|
|
|
|
isCandidate = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!isCandidate)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
impl->bindingsFlags[i] |= BINDING_FLAG_AUTOCOMPLETE;
|
|
|
|
|
|
|
|
if (cmd.candidateCount == 0 || len < cmd.autocompletedLen)
|
|
|
|
cmd.autocompletedLen = (uint16_t) len;
|
|
|
|
|
|
|
|
++cmd.candidateCount;
|
|
|
|
|
|
|
|
if (cmd.candidateCount == 1) {
|
|
|
|
cmd.firstCandidate = name;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t j = impl->cmdSize; j < cmd.autocompletedLen; ++j) {
|
|
|
|
if (cmd.firstCandidate[j] != name[j]) {
|
|
|
|
cmd.autocompletedLen = (uint16_t) j;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmd;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void printLiveAutocompletion(EmbeddedCli *cli) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
if (!IS_FLAG_SET(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED))
|
|
|
|
return;
|
|
|
|
|
|
|
|
AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer);
|
|
|
|
|
|
|
|
if (cmd.candidateCount == 0) {
|
|
|
|
cmd.autocompletedLen = impl->cmdSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
// print live autocompletion (or nothing, if it doesn't exist)
|
|
|
|
for (size_t i = impl->cmdSize; i < cmd.autocompletedLen; ++i) {
|
|
|
|
cli->writeChar(cli, cmd.firstCandidate[i]);
|
|
|
|
}
|
|
|
|
// replace with spaces previous autocompletion
|
|
|
|
for (size_t i = cmd.autocompletedLen; i < impl->inputLineLength; ++i) {
|
|
|
|
cli->writeChar(cli, ' ');
|
|
|
|
}
|
|
|
|
impl->inputLineLength = cmd.autocompletedLen;
|
|
|
|
cli->writeChar(cli, '\r');
|
|
|
|
// print current command again so cursor is moved to initial place
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
writeToOutput(cli, impl->cmdBuffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void onAutocompleteRequest(EmbeddedCli *cli) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
|
|
|
|
AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer);
|
|
|
|
|
|
|
|
if (cmd.candidateCount == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (cmd.candidateCount == 1 || cmd.autocompletedLen > impl->cmdSize) {
|
|
|
|
// can copy from index cmdSize, but prefix is the same, so copy everything
|
|
|
|
memcpy(impl->cmdBuffer, cmd.firstCandidate, cmd.autocompletedLen);
|
|
|
|
if (cmd.candidateCount == 1) {
|
|
|
|
impl->cmdBuffer[cmd.autocompletedLen] = ' ';
|
|
|
|
++cmd.autocompletedLen;
|
|
|
|
}
|
|
|
|
impl->cmdBuffer[cmd.autocompletedLen] = '\0';
|
|
|
|
|
|
|
|
writeToOutput(cli, &impl->cmdBuffer[impl->cmdSize]);
|
|
|
|
impl->cmdSize = cmd.autocompletedLen;
|
|
|
|
impl->inputLineLength = impl->cmdSize;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// with multiple candidates when we already completed to common prefix
|
|
|
|
// we show all candidates and print input again
|
|
|
|
// we need to completely clear current line since it begins with invitation
|
|
|
|
clearCurrentLine(cli);
|
|
|
|
|
|
|
|
for (int i = 0; i < impl->bindingsCount; ++i) {
|
|
|
|
// autocomplete flag is set for all candidates by last call to
|
|
|
|
// getAutocompletedCommand
|
|
|
|
if (!(impl->bindingsFlags[i] & BINDING_FLAG_AUTOCOMPLETE))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
const char *name = impl->bindings[i].name;
|
|
|
|
|
|
|
|
writeToOutput(cli, name);
|
|
|
|
writeToOutput(cli, lineBreak);
|
|
|
|
}
|
|
|
|
|
|
|
|
writeToOutput(cli, impl->invitation);
|
|
|
|
writeToOutput(cli, impl->cmdBuffer);
|
|
|
|
|
|
|
|
impl->inputLineLength = impl->cmdSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void clearCurrentLine(EmbeddedCli *cli) {
|
|
|
|
PREPARE_IMPL(cli);
|
|
|
|
size_t len = impl->inputLineLength + strlen(impl->invitation);
|
|
|
|
|
|
|
|
cli->writeChar(cli, '\r');
|
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
|
|
cli->writeChar(cli, ' ');
|
|
|
|
}
|
|
|
|
cli->writeChar(cli, '\r');
|
|
|
|
impl->inputLineLength = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void writeToOutput(EmbeddedCli *cli, const char *str) {
|
|
|
|
size_t len = strlen(str);
|
|
|
|
|
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
|
|
cli->writeChar(cli, str[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool isControlChar(char c) {
|
|
|
|
return c == '\r' || c == '\n' || c == '\b' || c == '\t' || c == 0x7F;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool isDisplayableChar(char c) {
|
|
|
|
return (c >= 32 && c <= 126);
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint16_t fifoBufAvailable(FifoBuf *buffer) {
|
|
|
|
if (buffer->back >= buffer->front)
|
|
|
|
return (uint16_t) (buffer->back - buffer->front);
|
|
|
|
else
|
|
|
|
return (uint16_t) (buffer->size - buffer->front + buffer->back);
|
|
|
|
}
|
|
|
|
|
|
|
|
static char fifoBufPop(FifoBuf *buffer) {
|
|
|
|
char a = '\0';
|
|
|
|
if (buffer->front != buffer->back) {
|
|
|
|
a = buffer->buf[buffer->front];
|
|
|
|
buffer->front = (uint16_t) (buffer->front + 1) % buffer->size;
|
|
|
|
}
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool fifoBufPush(FifoBuf *buffer, char a) {
|
|
|
|
uint16_t newBack = (uint16_t) (buffer->back + 1) % buffer->size;
|
|
|
|
if (newBack != buffer->front) {
|
|
|
|
buffer->buf[buffer->back] = a;
|
|
|
|
buffer->back = newBack;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool historyPut(CliHistory *history, const char *str) {
|
|
|
|
size_t len = strlen(str);
|
|
|
|
// each item is ended with \0 so, need to have that much space at least
|
|
|
|
if (history->bufferSize < len + 1)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// remove str from history (if it's present) so we don't get duplicates
|
|
|
|
historyRemove(history, str);
|
|
|
|
|
|
|
|
size_t usedSize;
|
|
|
|
// remove old items if new one can't fit into buffer
|
|
|
|
while (history->itemsCount > 0) {
|
|
|
|
const char *item = historyGet(history, history->itemsCount);
|
|
|
|
size_t itemLen = strlen(item);
|
|
|
|
usedSize = ((size_t) (item - history->buf)) + itemLen + 1;
|
|
|
|
|
|
|
|
size_t freeSpace = history->bufferSize - usedSize;
|
|
|
|
|
|
|
|
if (freeSpace >= len + 1)
|
|
|
|
break;
|
|
|
|
|
|
|
|
// space not enough, remove last element
|
|
|
|
--history->itemsCount;
|
|
|
|
}
|
|
|
|
if (history->itemsCount > 0) {
|
|
|
|
// when history not empty, shift elements so new item is first
|
|
|
|
memmove(&history->buf[len + 1], history->buf, usedSize);
|
|
|
|
}
|
|
|
|
memcpy(history->buf, str, len + 1);
|
|
|
|
++history->itemsCount;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const char *historyGet(CliHistory *history, uint16_t item) {
|
|
|
|
if (item == 0 || item > history->itemsCount)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
// items are stored in the same way (separated by \0 and counted from 1),
|
|
|
|
// so can use this call
|
|
|
|
return embeddedCliGetToken(history->buf, item);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void historyRemove(CliHistory *history, const char *str) {
|
|
|
|
if (str == NULL || history->itemsCount == 0)
|
|
|
|
return;
|
|
|
|
char *item = NULL;
|
|
|
|
uint16_t itemPosition;
|
|
|
|
for (itemPosition = 1; itemPosition <= history->itemsCount; ++itemPosition) {
|
|
|
|
// items are stored in the same way (separated by \0 and counted from 1),
|
|
|
|
// so can use this call
|
|
|
|
item = embeddedCliGetTokenVariable(history->buf, itemPosition);
|
|
|
|
if (strcmp(item, str) == 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
item = NULL;
|
|
|
|
}
|
|
|
|
if (item == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
--history->itemsCount;
|
|
|
|
if (itemPosition == (history->itemsCount + 1)) {
|
|
|
|
// if this is a last element, nothing is remaining to move
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t len = strlen(item);
|
|
|
|
size_t remaining = (size_t) (history->bufferSize - (item + len + 1 - history->buf));
|
|
|
|
// move everything to the right of found item
|
|
|
|
memmove(item, &item[len + 1], remaining);
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos) {
|
|
|
|
if (tokenizedStr == NULL || pos == 0)
|
|
|
|
return CLI_TOKEN_NPOS;
|
|
|
|
uint16_t i = 0;
|
|
|
|
uint16_t tokenCount = 1;
|
|
|
|
while (true) {
|
|
|
|
if (tokenCount == pos)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (tokenizedStr[i] == '\0') {
|
|
|
|
++tokenCount;
|
|
|
|
if (tokenizedStr[i + 1] == '\0')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tokenizedStr[i] != '\0')
|
|
|
|
return i;
|
|
|
|
else
|
|
|
|
return CLI_TOKEN_NPOS;
|
|
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif // EMBEDDED_CLI_IMPL_GUARD
|
|
|
|
#endif // EMBEDDED_CLI_IMPL
|