OS  v1.7.5
Documentation
Loading...
Searching...
No Matches
AT Command Line Interface (CLI)

Overview

A command-line interface (CLI) is a way to interact directly with the software of an embedded system in the form of text commands and responses. It can be seen as a typed set of commands to produce a result, but here, the commands are typed in real-time by a user through a specific interface, for example, UART, USB, LAN, etc.

A CLI is often developed to aid initial driver development and debugging. This CLI might become the interface (or one of the interfaces) used by a sophisticated end-user to interact with the product. Think of typing commands to control a machine, or perhaps for low-level access to the control system as a development tool, tweaking time-constants and monitoring low-level system performance during testing.

The components of the CLI

The provided development API parses and handles input commands, following a simplified form of the extended AT-commands syntax.

atcli
AT CLI implementation

As seen in the figure, the CLI has a few components described below:

  • Input Handler : It is responsible for collecting incoming data from the input in the form of ASCII characters inside a buffer. When this buffer is ready by receiving an EOL(End-Of-Line) byte, it notifies the validator to perform the initial checks.
  • Validator : Take the input string and perform three checks over it:
    1. The input matches one of the subscribed commands.
    2. The input matches one of the default commands.
    3. The input is unknown
  • Pre-Parser : Takes the input if the validator asserts the first check. It is responsible for syntax validation and classification. Also, prepares the input argument for the next component.
  • Callback or Post-Parser : If input at the pre-parser is valid, the respective command callback is invoked. Here, the application writer is free to handle the command execution and the output response.
  • Output printer : Takes all the return status of the previous components to print out a response at the output.
Remarks
Here, Input and Output should be provided by the application writer, for example, if a UART interface is chosen, the input should take the received bytes from an ISR and the output is a function to print out a single byte.

Supported syntax

The syntax is straightforward and the rules are provided below:

  • All command lines must start with AT and end with an EOL character. By default, the CLI uses the carriage return character. (We will use CR to represent a carriage return character in this document).
  • AT commands are case-insensitive
  • Only four types of AT commands are allowed:
    • Acting (cli::commandType::ACT) : This is the simplest type of commands that can be subscribed. It's normally used to execute the action that the command should do. This type does not take arguments or modifiers, for example : AT+CMD
    • Read (cli::commandType::READ) : This type of command allows you to read or test a value already configured for the specified parameter. Only one argument is allowed. for example "AT+CMD?" or "AT+CMD?PARAM1"
    • Test (cli::commandType::TEST) : These types of commands allow you to get the values that can be set for its parameters. No parameters are allowed here. Example "AT+CMD=?"
    • Parameter Set (cli::commandType::PARA) : These types of commands allow n arguments to be passed for setting parameters, for example: AT+CMD=x,y If none of the types is given at the input, the command response will be ERROR
  • The possible output responses are:
    • OK: Indicates the successful execution of the command.
    • ERROR: A generalized message to indicate failure in executing the command.
    • UNKNOWN : The input command is not subscribed.
    • NOT ALLOWED : The command syntax is not one of the allowed types.
    • User-defined: A custom output message defined by the application writer.
    • NONE : No response.

All responses are followed by a CR LF

Errors generated during the execution of these AT commands could be due to the following reasons:

  • Incorrect syntax/parameters of the AT command
  • Bad parameters or not allowed operations defined by the application writer.

In case of an error, the string ERROR or "ERROR:<error_no>" are displayed.

Setting up an AT-CLI instance

Before starting the CLI development, the corresponding instance must be defined; a variable of class qOS::commandLineInterface. The instance should be initialized using the qOS::commandLineInterface::setup() method.

Subscribing commands to the parser

The AT CLI is able to subscribe to any number of custom AT commands. For this, the qOS::commandLineInterface::add() method should be used.

This function subscribes the CLI instance to a specific command with an associated callback function, so that the next time the required command is sent to the CLI input, the callback function will be executed. The CLI parser only analyzes commands that follow the simplified AT-Commands syntax already described.

Writing a command callback

The command callback should be coded by the application writer. Here, the following prototype should be used:

cli::response CMD_Callback( cli::handler_t h ) {
/* TODO : The command callback */
}

The callback takes one argument of type qOS::cli::handler_t and returns a single value. The input argument it's just a pointer to public data of the CLI instance where the command it subscribed to. From the callback context, can be used to print out extra information as a command response, parse the command parameters, and query properties with crucial information about the detected command, like the type, the number of arguments, and the subsequent string after the command text. To see more details please check the qOS::cli::handler_t class reference.

The return value (an enum of type qOS::cli::response ) determines the response shown by the Output printer component. The possible allowed values are:

  • cli::response::OK : as expected, print out the OK string.
  • cli::response::ERROR : as expected, print out the ERROR string.
  • cli::ERROR_CODE(no) : Used to indicate an error code. This code is defined by the application writer and should be a value between 1 and 32766. For example, a return value of cli::ERROR_CODE(15), will print out the string ERROR:15.
  • cli::response::NO_RESPONSE : No response will be printed out.

A simple example of how the command callback should be coded is shown below:

cli::response CMD_Callback( cli::handler_t h ) {
cli::response Response = cli::response::NO_RESPONSE;
int arg1 = 0;
float arg2 = 0;
/*check the command-type*/
switch ( h.getType() ) {
case cli::commandType::PARA:
if ( h.getNumArgs() > 0 ) {
arg1 = h.getArgInt( 1 ); /*get the first argument as integer*/
if ( h.getNumArgs() > 1 ) {
arg2 = h.getArgFlt( 2 ); /*get the second argument as float*/
sprintf( h.output, "arg1 = %d arg2 = %f", arg1, arg2 );
Response = cli::response::NO_RESPONSE;
}
else {
Response = cli::response::ERROR;
}
break;
case cli::commandType::TEST:
h.writeOut( "inmediate message" );
Response = cli::response::OK;
break;
case cli::commandType::READ:
strcpy( h.output , "Test message after the callback" );
Response = cli::response::OK;
break;
case cli::commandType::ACT:
Response = cli::response::OK;
break;
default:
Response = cli::response::ERROR;
break;
}
return Response;
}

Handling the input

Input handling is simplified using the provided methods of this class. The qOS::commandLineInterface::isrHandler() function are intended to be used from the interrupt context. This avoids any kind of polling implementation and allows the CLI application to be designed using an event-driven pattern.

The two overloads for this method feed the parser input, the first one with a single character and the second with a string. The application writer should call one of these functions from the desired hardware interface, for example, from a UART receive ISR.

If there is no intention to feed the input from the ISR context, the methods qOS::commandLineInterface::raise() or qOS::commandLineInterface::exec() can be called at demand from the base context. As expected, both functions send the string to the specified CLI. The difference between both methods is that qOS::commandLineInterface::raise() sends the command through the input, marking it as ready for parsing and acting as the Input handler component.

The qOS::commandLineInterface::exec(), on the other hand, executes the components of Pre-parsing and Postparsing bypassing the other components, including the Output printer, so that it must be handled by the application writer.

Note
All functions involved with the component Input-handler, ignores non-graphic characters and cast any uppercase to lowercase.

Running the CLI parser

The parser can be invoked directly using the qOS::commandLineInterface::run() method. Almost all the components that make up the CLI are performed by this method, except for the Input Handler, which should be managed by the application writer itself.

In this way, the writer of the application must implement the logic that leads this function to be called when the input-ready condition is given.

The simple approach for this is to check the return value of any of the input feeder APIs and set a notification variable when they report a ready input. Later in the base context, a polling job should be performed over this notification variable, running the parser when their value is true, then clearing the value after to avoid unnecessary overhead.

The recommended implementation is to leave this job handled by a task instead of coding the logic to know when the CLI should run. For this, an overload of qOS::core::add() is provided accepting an instance of the CLI. This overload adds a task to the scheduling scheme running an AT Command Line Interface and is treated as an event-triggered task. The address of the parser instance will be stored in the TaskData storage-Pointer.

After invoked, both CLI and task are linked together in such a way that when an input-ready condition is given, a notification event is sent to the task launching the CLI components. As the task is event-triggered, there is no additional overhead and the writer of the application can assign a priority value to balance the application against other tasks in the scheduling scheme.

A CLI example

The following example demonstrates the usage of a simple command-line interface using the UART peripheral with two subscribed commands :

  • A command to write and read the state of a GPIO pin "at+gpio".
  • A command to retrieve the compilation timestamp "at+info". First, let's get started defining the required objects to set up the CLI instance:

    #define CLI_MAX_INPUT_BUFF_SIZE ( 128 )
    #define CLI_MAX_OUTPUT_BUFF_SIZE ( 128 )
    task CLI_Task;
    commandLineInterface CLI_Object;
    cli::command AT_GPIO, AT_INFO;
    char CLI_Input[ CLI_MAX_INPUT_BUFF_SIZE ];
    char CLI_Output[ CLI_MAX_OUTPUT_BUFF_SIZE ];
    /*Command callbacks*/
    cli::response AT_GPIO_Callback( cli::handler_t h );
    cli::response AT_INFO_Callback( cli::handler_t h );

Then the CLI instance is configured by subscribing commands and adding the task to the OS. A wrapper function is required here to make the UART output-function compatible with the CLI API.

void CLI_OutputChar_Wrapper( void *sp, const char c ) { /*CLI output function*/
(void)sp; /*unused*/
HAL_UART_WriteChar( UART1, c );
}
/*==================================================================*/
int main( void ) {
HAL_Setup();
os.init( HAL_GetTick, nullptr );
CLI_Object.setup( BSP_UART_PUTC, CLI_Input, sizeof(CLI_Input),
CLI_Output, sizeof(CLI_Output) );
CLI_Object.add( AT_GPIO, "at+gpio", AT_GPIO_Callback,
QATCLI_CMDTYPE_ACT | QATCLI_CMDTYPE_READ |
QATCLI_CMDTYPE_TEST | QATCLI_CMDTYPE_PARA | 0x22 );
CLI_Object.add( AT_INFO, "at+info", AT_INFO_Callback,
QATCLI_CMDTYPE_ACT );
os.add( CLI_Task, CLI_Object, core::LOWEST_PRIORITY );
os.run();
return 0;
}
bool run(void) noexcept
Executes the scheduling scheme. It must be called once after the task pool has been defined.
bool add(task &Task, taskFcn_t callback, const priority_t p, const duration_t t, const iteration_t n, const taskState s=taskState::ENABLED_STATE, void *arg=nullptr) noexcept
Add a task to the scheduling scheme. The task is scheduled to run every t time units,...
bool init(const getTickFcn_t tFcn=nullptr, taskFcn_t callbackIdle=nullptr) noexcept
Task Scheduler initialization. This core method is required and must be called once in the applicatio...

The CLI input is feeded from the interrupt context by using the UART receive ISR:

void interrupt HAL_UART_RxInterrupt( void ) {
char received;
received = HAL_HUART_GetChar( UART1 );
CLI_Object.isrHandler( received ); /*Feed the CLI input*/
}

Finally, the command callbacks are later defined to perform the requested operations.

cli::response AT_GPIO_Callback( cli::handler_t h ) {
cli::response RetValue = cli::response::ERROR;
int pin, value;
switch ( h.getType() ) {
case cli::commandType::ACT: /*< AT+gpio */
RetValue = cli::response::OK;
break;
case cli::commandType::TEST: /*< AT+gpio=? */
h.writeOut( "+gpio=<pin>,<value>\r\n" );
h.writeOut( "+gpio?\r\n" );
RetValue = cli::response::NO_RESPONSE;
break;
case cli::commandType::READ: /*< AT+gpio? */
sprintf( h.Output, "0x%08X", HAL_GPIO_Read( GPIOA ) );
RetValue = cli::response::NO_RESPONSE;
break;
case cli::commandType::PARA: /*< AT+gpio=<pin>,<value> */
pin = h.getArgInt( 1 );
value = h.getArgInt( 2 );
HAL_GPIO_WRITE( GPIOA, pin, value );
RetValue = cli::response::OK;
break;
default : break;
}
return RetValue;
}
/*==================================================================*/
cli::response AT_INFO_Callback( cli::handler_t h ) {
cli::response RetValue = cli::response::ERROR;
switch ( param.getType() ) {
case cli::commandType::ACT: /*< AT+info */
util::strcpy( h.output, "Compilation: " __DATE__ " " __TIME__ );
RetValue = cli::response::NO_RESPONSE;
break;
default :
break;
}
return RetValue;
}