We use an RTOS to schedule all the tasks (FreeRTOS), if you are unfamiliar with what an RTOS does here is a good short video explaining the fundamentals... video
There are several threads running in the system.
- One thread for each input device (ACM/CDC, UART, Network), if there are two USB ports specified in the config then there will be a thread for each
- The main command thread which is where all gcodes and comamnds run
- A timer thread that can run tasks regularly, but cannot block execution
- Various other threads depending on what options have been enabled (for instance the player module runs a thread which reads gcode from the sdcard and sends it to the command thread to be executed)
All commands and gcodes must be run from the command thread, which gets its command lines from a message queue which the other threads push lines onto. Checks are made to make sure you don't accidently try to execute a command from a different thread, although this cannot be relied on and is only enabled in debug builds.
TBD See tools/examples/module-template.*
TBD
TBD
TBD
The player module is a good example of how to get gcodes from an sdcard file and feed them intot the command thread. Basicallt the line is pushed onto the message queue, although there are some quirks qhere you need to make sure you don't saturate that queue.
Basically any thread other than the command thread must put things on the message queue to get them executed.
However if a command needs to run a gcode it can call
dispatch_line() directly as it is already in the command thread, however
this cannot handle a lot of lines, maybe one or two gcodes, because stalling
the command thread means nothing else will get done, and all other ports will
be blocked. Examples here are the test command in the CommandShell, although
some tests will stall the command thread, and probably shouldn't. Basically
if enough gcodes are issued to fill the block queue, then the command thread
will stall waiting for room. It is a good idea to not issue more gcodes than
the size of the block queue.
If a module must do so then it should be run in a thread and should issue the gcodes in a controlled way as in the player thread. An alternative, as mentioned later, is to run in the comms thread for that console, basically simulating a stream of gcodes from a host, then the console gets stalled but that is ok. Another alternative is to schedule the issuing of gcodes from a slowtimer, one or two per cycle.
There is also a callback in_command_ctx that can be requested (by setting
want_command_ctx = true in configure()) that is called when the command
thread is idle or running.
This callback is running in the command thread context so can issue send
commands direct to dispatch_line(). The thread can arrange by various means
to have this callback issue the commands it wants.
TBD
- blocking or stalling the command thread
- blocking the timer threads
An example of a badly implemented command is the drillingcycles module which was ported from V1. It violates several of the above advice. The peck cycle could sit in the peck loop issuing gcodes that fill up the block queue, then blocking the command thread, which means nothing would get done from any comms thread as there is only one command thread. Although this is not fatal as eventually the block queue would clear and the peck cycle would end, it is however undesirable.
Calling safe_sleep() from a thread other than the command thread. This call
allows the command thread to sleep but still service queries, however that is
not needed in other threads as they can simply call vTaskDelay() instead as
queries will be handled by the comamnd thread so long as it is not stalled.
The same is true for things like wait_for_idle().
Concurrent access to globals like current position and other status. In
general one needs to be very careful of race conditions and concurrent access
to variables when running threads, use of mutex and message queues is usually
required. A good understanding of FreeRTOS is recommended. An example is
setting the callback function, in the caller it checks if the callback_fnc
is nullptr, and if not then it calls callback_fnc(). If this is being set
to nullptr in a separate thread or interrupt, then it could get set to
nullptr in between the check and the call, causing a crash. (This actually
happened in stepticker).