Close

The Monitor Task and the Command Processor

A project log for Careless WSPR

A desultorily executed Weak Signal Propagation Reporter beacon.

ziggurat29ziggurat29 08/08/2019 at 19:200 Comments

Summary

The skeletal implementation of the Monitor task is implemented.

Deets

The Monitor task is a simple command line interface over a stream interface.  In this project, that stream will the the USB CDC interface.

The design is fairly simple:  incoming data is built into a fixed-size command line buffer, and when a CR or LF is received, that is interpreted as the end of line, and it is subsequently parsed and processed accordingly.  The command line buffer is simply a statically allocated character array of 128 chars.  This is expected to be plenty (maybe even too much, but we'll see what evolves).

The FreeRTOS task is straightforward:

  1. define the FreeRTOS structures needed (the thread handle, the stack, the task control block, and the thread function)
  2. the task exposes a pointer to a stream interface.  This allows binding of the command process to an arbitrary stream.
  3. the thread function a loop invoking the command processor function

The Command Processor

The command processor is realized with a generic component I use in several projects.  This generic component defines a structure:

struct CmdProcEntry
{
	const char*	_pszCommand;
	CmdProcRetval (*_pfxnHandler) ( const IOStreamIF* pio, 
                const char* pszszTokens );
	const char* _pszHelp;
};

The intention is that your application will define an array of these structures somewhere.  The entries in that array consist of:

  1. the text that is the command
  2. a function that handles the command along with any additional parameters
  3. a short text that is used for the 'help' command

There is a function exposed:

CmdProcRetval CMDPROC_process ( const IOStreamIF* pio, 
        const CmdProcEntry* acpe, size_t nAcpe );

This takes the stream on which the command processor is operating and the application-specific array of command entries.  This function will build the command line buffer and support things like backspace, etc.  When an end-of-line character (CR or LF) is encountered, it will parse the first whitespace delimited token and search in the array of command entries for the handler for that command.  It will then invoke the handler function.  This lets me easily reuse this common capability amongst several projects.  The project-specific part is to define the commands you want and to perform some action when they are received.

The most basic command is 'help', which works two ways:

  1. invoked by itself, it will list all the commands in the repertoire
  2. invoked with a token, it will search the command list and emit the help text for that specific command

The handler for 'help' is straightforward and shows how such is constructed:

static CmdProcRetval cmdhdlHelp ( const IOStreamIF* pio, const char* pszszTokens )
{
	//get next token; we will get help on that
	int nIdx;
	if ( NULL != pszszTokens && '\0' != *pszszTokens &&
		-1 != ( nIdx = CMDPROC_findProcEntry ( pszszTokens, 
                        g_aceCommands, g_nAceCommands ) ) )
	{
		//emit help information for this one command
		_cmdPutString ( pio, g_aceCommands[nIdx]._pszHelp );
		_cmdPutString ( pio, "\r\n" );
	}
	else
	{
		//if unrecognised command
		if ( NULL != pszszTokens && '\0' != *pszszTokens )
		{
			_cmdPutString ( pio, "The command '" );
			_cmdPutString ( pio, pszszTokens );
			_cmdPutString ( pio, "' is not recognized.\r\n" );
		}

		//list what we've got
		_cmdPutString ( pio, "help is available for:\r\n" );
		for ( nIdx = 0; nIdx < g_nAceCommands; ++nIdx )
		{
			_cmdPutString ( pio, g_aceCommands[nIdx]._pszCommand );
			_cmdPutString ( pio, "\r\n" );
		}
	}

	return CMDPROC_SUCCESS;
}

 additionally, in debug build, I provide the 'diag' command:

static CmdProcRetval cmdhdlDiag ( const IOStreamIF* pio, const char* pszszTokens )
{
	//list what we've got
	_cmdPutString ( pio, "diagnostic vars:\r\n" );
	char ach[80];
	sprintf ( ach, "Heap: free now: %u, min free ever: %u\r\n", 
                g_nHeapFree, g_nMinEverHeapFree );
	_cmdPutString ( pio, ach );

	sprintf ( ach, "GPS max RX queue: %u\r\n", g_nMaxGPSRxQueue );
	_cmdPutString ( pio, ach );

	sprintf ( ach, "Monitor max RX queue %u, max TX queue %u\r\n", 
                g_nMaxCDCRxQueue, g_nMaxCDCTxQueue );
	_cmdPutString ( pio, ach );

	sprintf ( ach, "Task: Default: min stack free %u\r\n", 
                g_nMinStackFreeDefault*sizeof(uint32_t) );
	_cmdPutString ( pio, ach );

	sprintf ( ach, "Task: Monitor: min stack free %u\r\n", 
                g_nMinStackFreeMonitor*sizeof(uint32_t) );
	_cmdPutString ( pio, ach );

	return CMDPROC_SUCCESS;
}

This dumps the memory usage statistics we collected in the Default task.

Another command I usually implement is a 'dump' which simply does a hex dump of an arbitrary memory location.  It doesn't really substitute for debugging with the ST-Link, but it is useful in a pinch -- especially when used with the output map file.  'reboot' is also a simple one that is handy at times.

Other commands will be implemented as the project is continued to be fleshed-out.

The Monitor Task

To run the command processor in the project, you just need to do a couple things:

  1. create the FreeRTOS task that services the processor
  2. bind the stream interface onto the processor

Since we're going to start creating our own tasks beyond the default one, I create a function:

void __startWorkerTasks ( void )
{
	//kick off the monitor thread, which handles the user interactions
	{
	osThreadStaticDef(taskMonitor, thrdfxnMonitorTask, osPriorityNormal, 0, 
                COUNTOF(g_tbMonitor), g_tbMonitor, &g_tcbMonitor);
	g_thMonitor = osThreadCreate(osThread(taskMonitor), NULL);
	}
}

where I kick off the project-specific tasks.  You have to define some things:

  1. a 'task handle' object
  2. a 'task buffer' (really, this is the task's stack)
  3. a 'task control block'
  4. a 'task function'

Since this project's tasks live forever, I define them as static tasks.  This simply keeps the allocation of the above mentioned items out of the heap and into globally defined objects.  There's not really anything magical to this other than saving a small amount of RAM (otherwise used by arena headers in the heap), and a little more visibility into the memory footprint.  (And ostensibly reduced heap fragmentation potential, but that wouldn't be an issue here since they happen to be allocated before anything else and are permanent residents.)

The memory allocations are straightforward and you can use the ones generated for the 'default' task as a pattern.  I put those definitions in the task's own source file, though, and define the objects as 'extern' in the header.  Then include that header in main.c so they have visibility.  The generated code from CubeMX has much more in main.c than I would like for my tastes, but I find it's better to not fight the code generator -- it's just an exercise in frustration.

The 'task_monitor.h' is very simple, exposing just what is required:

#include "cmsis_os.h"
#include "system_interfaces.h"

extern osThreadId g_thMonitor;
extern uint32_t g_tbMonitor[ 256 ];
extern osStaticThreadDef_t g_tcbMonitor;

extern const IOStreamIF* g_pMonitorIOIf;	//the IO device to which the monitor is attached

void thrdfxnMonitorTask ( void const* argument );

The implementation of the task thread function in 'task_monitor.c' is also very simple because I put the machinery of the command processor in the separate implementation described above

#include "task_monitor.h"
#include "task_notification_bits.h"
#include "command_processor.h"
#include "CarelessWSPR_commands.h"

//the task that runs an interactive monitor on the USB data
osThreadId g_thMonitor = NULL;
uint32_t g_tbMonitor[ 256 ];
osStaticThreadDef_t g_tcbMonitor;

const IOStreamIF* g_pMonitorIOIf = NULL;	//the IO device

void thrdfxnMonitorTask ( void const* argument )
{
	for(;;)
	{
		while ( CMDPROC_QUIT != CMDPROC_process ( g_pMonitorIOIf, 
                        g_aceCommands, g_nAceCommands ) )
		{
			//(if you wanted to process the return value, but I don't)
		}
		//(we would get here if one of our handlers returned CMDPROC_QUIT,
                //but none of ours do, and even if they did, we do not want this
                //task ever to quit, so we infinite loop in the forever loop, above.)
	}
}

Wiring It In

Once that is done, then you just need to bind the interface to the USB CDC (or you could use UART2 if you wanted) and kick off the task.  I do this right after I do the serial init's in the Default task:

	//bind the interfaces to the relevant devices
	g_pMonitorIOIf = &g_pifCDC;		//monitor is on USB CDC

And then before we start the default task's 'forever' loop, we start up the other tasks:

	//start up worker threads
	__startWorkerTasks();

And that is all there is to it!  For adding commands, you just define new

handler entries and add them to the list.

Here's an example of a usage session:

Not especially fancy out-of-box, but all manner of prettification is possible if desired.  Shown is  dump of the heap on this particular build, the address of ucHeap was found in the output.map file in the 'Debug' directory.  You can see the arena header of the first block, and the 'empty data' fill pattern.

Finally, my NEO-6m modules have arrived, so it's probably time to work on the GPS task.

Next

Implementing the GPS task

Discussions