Skip to content

Adding custom Tools and Modules to µCNC

Paciente8159 edited this page Feb 14, 2024 · 40 revisions

Jump to section

Adding custom modules to µCNC

IMPORTANT NOTE: Version 1.7 implemented breaking changes to modules declarations. Please check the modules releases to get the right modules for your version repository

NOTE: Version 1.4.6 implemented changes to module initialization. Also additional modules were moved to a new repository

µCNC has implemented a module system that allows the user to perform custom actions that get executed in an event/delegate fashion style similar to what is done with C#. Multiple callbacks functions can be attached to the same event. These modules can be quite useful and perform several things like adding custom custom gcodes to perform actions, or modifying IO states if a given condition is verified. µCNC already has a few useful modules like PID controller, Encoder module, TMC drivers support and custom G/M code support. These modules are already integrated in the Web Config Tool and can be added via this tool.

µCNC existing events/delegates

Without having to modify core code inside µCNC it is possible to listen to several already existing events. Here is a list of current events:

Event name Enable option Description
gcode_parse ENABLE_PARSER_MODULES Fires when a gcode command was not captured/understood by the core parser is trying to parse the code
gcode_exec ENABLE_PARSER_MODULES Fires when a gcode command that was not captured/understood by the core parser is intercepted by gcode_parse event an recognized as an extended gcode command
gcode_exec_modifier ENABLE_PARSER_MODULES Fires prior to gcode execution phase and can be used to modify the parsed command or execute custom actions or issue motions before gcode execution
grbl_cmd ENABLE_PARSER_MODULES Fires when a custom/unknown '$' grbl type command is received
parse_token ENABLE_PARSER_MODULES Fires na custom/unknown token/char is received for further processing
cnc_reset ENABLE_MAIN_LOOP_MODULES Fires when µCNC resets
rtc_tick ENABLE_MAIN_LOOP_MODULES Fires every millisecond. This code runs inside the RTC interrupt. Do not run long routines here. This is for time critical (periodic) tasks.
cnc_dotasks ENABLE_MAIN_LOOP_MODULES Fires on the main loop running. Any repeating task should be hooked here
cnc_stop ENABLE_MAIN_LOOP_MODULES Fires when a halt/stop condition is triggered
cnc_exec_cmd_error ENABLE_MAIN_LOOP_MODULES Fires when an invalid command is received
settings_change ENABLE_SETTINGS_MODULES Fires when a $ setting is changed
settings_load ENABLE_SETTINGS_MODULES Fires when settings are loaded from memory
settings_save ENABLE_SETTINGS_MODULES Fires when settings are saved into memory
settings_erase ENABLE_SETTINGS_MODULES Fires when settings are erased/reset
protocol_send_cnc_settings ENABLE_SETTINGS_MODULES Fires when printing settings values
protocol_send_cnc_info ENABLE_SYSTEM_INFO Fires when printing response to $I command
send_pins_states ENABLE_IO_MODULES Fires when $P command is printing the pins states
input_change ENABLE_IO_MODULES Fires when a generic input pin changes state
probe_enable ENABLE_MOTION_MODULES Fires when the probe is enabled
probe_disable ENABLE_MOTION_MODULES Fires when the probe is disabled
mc_line_segment ENABLE_MOTION_CONTROL_MODULES Fires when a line segment is about to be sent from the motion control to the planner

Each of these events exposes a delegate and and event that have the following naming convention:

<event_name>_delegate -> type of a function pointer that defines the function prototype to be called by the event handler <event_name>_event -> pointer to the event that contains all the subscribers that are going to be called by the event handler mod_<event_name>_hook -> the event handler that is called inside the core code and calls and executes every subscriber callback

For example cnc_dotasks exposes cnc_dotasks_delegate and cnc_dotasks_event that will enable to create an attach a listener that gets called when the event executed inside the core code. This is done by the call to mod_cnc_dotasks_hook inside cnc_dotasks main loop function.

Most of these events have no input or output arguments. That translates to a function pointer like void (*fp) (void). The exceptions are:

gcode_parse

Input args:

gcode_parse_args_t* - Pointer to a struct of type gcode_parse_args_t. The gcode_parse_args_t struct is defined like this:

typedef struct gcode_parse_args_
{
	// This is the GCode word being parsed (usually 'G' or 'M')
	unsigned char word;
	// This is the GCode word argument converted to uint8_t format (example 98)
	uint8_t code;
	// The parser current error code (STATUS_GCODE_EXTENDED_UNSUPPORTED)
	uint8_t error;
	// This is the actual GCode word argument parsed as float. Useful if code has mantissa or is bigger then 255 (example 98.1)
	float value;
	// The current parser state (struct)
	parser_state_t *new_state;
	// The current parser words argument values (struct)
	parser_words_t *words;
	// The current parser GCode command active words/groups
	parser_cmd_explicit_t *cmd;
} gcode_parse_args_t;

Returns:

uint8_t - Returns the error. 3 outcomes are possible:

  • The parser successfully intercepted and parsed the command (returning STATUS_OK).
  • While parsing and recognizing the Gcode word found an error or invalid value trying to parse it and returns an error code.
  • Ignores or does not recognize the current word an passes the current parser status error, received as a function argumento (STATUS_GCODE_EXTENDED_UNSUPPORTED) back to the event handler. The event handle recognizes this as the word not being recognized and continues to propagate the parsing by sending it to the next subscriber. In the end if no subscriber intercepts the word command the parser sends an error.

gcode_exec

Input args:

gcode_exec_args_t* - Pointer to a struct of type gcode_exec_args_t. The gcode_exec_args_t struct is defined like this:

typedef struct gcode_exec_args_
{
        // The current parser state (struct)
	parser_state_t *new_state;
        // The current parser words argument values (struct)
	parser_words_t *words;
        // The current parser GCode command active words/groups
	parser_cmd_explicit_t *cmd;
} gcode_exec_args_t;

Returns:

uint8_t - Returns the error. 3 outcomes are possible:

  • The parser successfully intercepted and parsed the command (returning STATUS_OK).
  • While parsing and recognizing the Gcode word found an error or invalid value trying to parse it and returns an error code.
  • Ignores or does not recognize the current word an passes the current parser status error, received as a function argumento (STATUS_GCODE_EXTENDED_UNSUPPORTED) back to the event handler. The event handle recognizes this as the word not being recognized and continues to propagate the parsing by sending it to the next subscriber. In the end if no subscriber intercepts the word command the parser sends an error.

itp_reset_rt_position

Input args:

float* - The float array or pointer with the new origin that will be fixed as the machine new coordinates

Returns:

nothing

Creating a new event listener

Creating a new event listener is easy. All current modules are inside the src/modules directory but it's not mandatory. It's just a matter of having them organized.

Let's look at an example in the Arduino IDE environment by creating and attaching and event listener to cnc_dotasks.

Add a new file .c to uCNC directory (same has uCNC.ino) and paste this code

//include cnc.h relative path
#include "src/cnc.h"

//preprocessor check if module is enabled
#ifdef ENABLE_MAIN_LOOP_MODULES

//custom function declaration
void my_custom_code(void);
//create a listener of type cnc_dotasks
CREATE_LISTENER(cnc_dotasks_delegate, my_custom_code);
//custom code implementation
void my_custom_code(void)
{
    // do something
}

#endif

This created the event listener that is of type cnc_dotasks. The only thing left to do is to attach it so that it gets called when cnc_dotasks is fired.

In the uCNC.ino it's just a matter of adding the listener to the event like this:

Open uCNC.ino

#include "src/cnc.h"

int main(void)
{
    //initializes all systems
    cnc_init();
    // add the listener to cnc_dotasks
    ADD_LISTENER(cnc_dotasks_delegate, my_custom_code, cnc_dotasks_event);
    for (;;)
    {
        cnc_run();
    }
}

That's it. Your custom function will run inside the main loop.

Adding custom tools to µCNC

µCNC tool is a bit different from modules but it's equally straight forward. tool.h file sets a struct with a set of function pointer that can be defined to create a custom tool. In the end you just need to add the tool in cnc_hal_config.h file by attributing it to a TOOL.

µCNC tool structure

The tool struct is like this:

typedef void (*tool_func)(void);
	typedef int16_t (*tool_pid_err_func)(void);
	typedef void (*tool_pid_upd_func)(int16_t);
	typedef int16_t (*tool_range_speed_func)(int16_t ,uint8_);
	typedef uint16_t (*tool_get_speed_func)(void);
	typedef void (*tool_set_speed_func)(int16_t);
	typedef void (*tool_coolant_func)(uint8_t);

	typedef struct
	{
		tool_func startup_code;			   /*runs any custom code after the tool is loaded*/
		tool_func shutdown_code;		   /*runs any custom code before the tool is unloaded*/
		tool_func pid_update;			   /*runs de PID update code needed to keep the tool at the desired speed/power*/
		tool_range_speed_func range_speed; /*converts core speed to tool speed*/
		tool_get_speed_func get_speed;	   /*gets the tool speed/power (converts from tool speed to core speed)*/
		tool_set_speed_func set_speed;	   /*sets the speed/power of the tool*/
		tool_coolant_func set_coolant;	   /*enables/disables the coolant*/
	} tool_t;

µCNC creating a dummy tool

This is an example for creating a dummy tool. Again all current tools are inside the src/hal/tool/tools directory but it's not mandatory. It's just a matter of having them organized.

Add a new file .c to uCNC directory (same has uCNC.ino) and paste this code

//include cnc.h relative path
#include "src/cnc.h"

//may be needed (contains the definition of bool)
#include <stdbool.h>

void dummy_tool_startup_code(void)
{
    //run code on tool loading
}

void dummy_tool_shutdown_code(void)
{
    //run code on tool unloading
}

void dummy_tool_set_speed(uint8_t value, bool invert)
{
    //do whatever. value is the absolute speed of the tool from 0 to 255. invert indicates the direction
}

int16_t dummy_tool_range_speed(int16_t value, uint8_t conv)
{
    // if conv is 0 convert from GCode S speed to tool IO control speed (to PWM value or other for example), if conv is not convert from IO control speed to GCode S speed
    return value;
}

uint8_t dummy_tool_get_speed(void)
{
   // this tool will always report speed 0 as an example
   return 0;
}

//declares the tool in ROM
//all unused function pointer must be initialized to NULL to prevent unexpected behavior
const tool_t __rom__ dummy_tool = {
    .startup_code = &dummy_tool_startup_code, //sets the function pointer
    .shutdown_code = &dummy_tool_shutdown_code, //sets the function pointer
    .set_speed = &dummy_tool_set_speed, //sets the function pointer
    .range_speed = &dummy_tool_range_speed, //sets the function pointer
    .set_coolant = NULL, //If you don't need to do nothing in a function you don't need to declare it. Just set the pointer to NULL.
    .pid_update = NULL,
    .get_speed = &dummy_tool_get_speed}; //sets the function pointer

That's it. The only thing left is to add the tool in cnc_hal_config.h Lets say this will be TOOL5

Open uCNC.ino add this to cnc_hal_config.h

#define TOOL5 dummy_tool

That's it. You can use your tool via M6 T5 command.