diff --git a/config.h b/config.h
index 984fc733..64f6da4b 100644
--- a/config.h
+++ b/config.h
@@ -529,6 +529,14 @@ Set to \ref On or 1 to enable experimental support for parameters.
#define NGC_PARAMETERS_ENABLE On
#endif
+/*! \def STRING_REGISTERS_ENABLE
+\brief
+Set to \ref On or 1 to enable experimental support for string registers.
+*/
+#if !defined STRING_REGISTERS_ENABLE || defined __DOXYGEN__
+#define STRING_REGISTERS_ENABLE Off
+#endif
+
/*! \def NGC_N_ASSIGN_PARAMETERS_PER_BLOCK
\brief
Maximum number of parameters allowed in a block.
diff --git a/core_handlers.h b/core_handlers.h
index e4e138bd..656f3c94 100644
--- a/core_handlers.h
+++ b/core_handlers.h
@@ -133,6 +133,8 @@ typedef bool (*write_tool_data_ptr)(tool_data_t *tool_data);
typedef bool (*read_tool_data_ptr)(tool_id_t tool_id, tool_data_t *tool_data);
typedef bool (*clear_tool_data_ptr)(void);
+typedef char* (*on_string_substitution_ptr)(char* input, char** output);
+
typedef struct {
uint32_t n_tools;
tool_data_t *tool; //!< Array of tool data, size _must_ be n_tools + 1
@@ -244,6 +246,7 @@ typedef struct {
on_set_axis_setting_unit_ptr on_set_axis_setting_unit;
on_gcode_message_ptr on_gcode_message; //!< Called on output of message parsed from gcode. NOTE: string pointed to is freed after this call.
on_gcode_message_ptr on_gcode_comment; //!< Called when a plain gcode comment has been parsed.
+ on_string_substitution_ptr on_string_substitution; //!< Called when something wants to process a string for param substitution or similar.
on_tool_selected_ptr on_tool_selected; //!< Called prior to executing M6 or after executing M61.
on_tool_changed_ptr on_tool_changed; //!< Called after executing M6 or M61.
on_toolchange_ack_ptr on_toolchange_ack; //!< Called from interrupt context.
diff --git a/gcode.c b/gcode.c
index 5936e92c..c2a641a9 100644
--- a/gcode.c
+++ b/gcode.c
@@ -43,6 +43,10 @@
#endif
#endif
+#if STRING_REGISTERS_ENABLE
+#include "string_registers.h"
+#endif
+
// NOTE: Max line number is defined by the g-code standard to be 99999. It seems to be an
// arbitrary value, and some GUIs may require more. So we increased it based on a max safe
// value when converting a float (7.2 digit precision) to an integer.
@@ -348,6 +352,10 @@ void gc_init (void)
#if NGC_EXPRESSIONS_ENABLE
ngc_flowctrl_init();
+ #if STRING_REGISTERS_ENABLE
+ string_registers_init();
+ #endif
+ ngc_expr_init();
#endif
#if NGC_PARAMETERS_ENABLE
ngc_modal_state_invalidate();
@@ -599,14 +607,16 @@ char *gc_normalize_block (char *block, status_code_t *status, char **message)
if(message && *message == NULL) {
#if NGC_EXPRESSIONS_ENABLE
if(!strncasecmp(comment, "DEBUG,", 6)) { // Debug message string substitution
- if(settings.flags.ngc_debug_out) {
+ if(settings.flags.ngc_debug_out && grbl.on_string_substitution) {
comment += 6;
- ngc_substitute_parameters(comment, message);
+ grbl.on_string_substitution(comment, message);
}
*comment = '\0'; // Do not generate grbl.on_gcode_comment event!
} else if(!strncasecmp(comment, "PRINT,", 6)) { // Print message string substitution
- comment += 6;
- ngc_substitute_parameters(comment, message);
+ if(grbl.on_string_substitution) {
+ comment += 6;
+ grbl.on_string_substitution(comment, message);
+ }
*comment = '\0'; // Do not generate grbl.on_gcode_comment event!
} else {
#endif
diff --git a/ngc_expr.c b/ngc_expr.c
index 2202a4f6..75030b09 100644
--- a/ngc_expr.c
+++ b/ngc_expr.c
@@ -31,6 +31,7 @@
#include "settings.h"
#include "ngc_expr.h"
#include "ngc_params.h"
+#include "core_handlers.h"
#define MAX_STACK 7
@@ -75,6 +76,8 @@ typedef enum {
NGCUnaryOp_Parameter // read setting/setting bit
} ngc_unary_op_t;
+static on_string_substitution_ptr on_string_substitution;
+
/*! \brief Executes the operations: /, MOD, ** (POW), *.
\param lhs pointer to the left hand side operand and result.
@@ -1061,4 +1064,28 @@ char *ngc_substitute_parameters (char *comment, char **message)
return *message;
}
+char* onNgcParameterSubstitution(char *input, char **output) {
+ char* result;
+ if (on_string_substitution) {
+ char *intermediate;
+ on_string_substitution(input, &intermediate);
+ result = ngc_substitute_parameters(intermediate, output);
+ free(intermediate);
+ } else {
+ result = ngc_substitute_parameters(input, output);
+ }
+
+ return result;
+}
+
+void ngc_expr_init(void) {
+ static bool init_ok = false;
+
+ if(!init_ok) {
+ init_ok = true;
+ on_string_substitution = grbl.on_string_substitution;
+ grbl.on_string_substitution = onNgcParameterSubstitution;
+ }
+}
+
#endif
diff --git a/ngc_expr.h b/ngc_expr.h
index d57678a0..9156cd82 100644
--- a/ngc_expr.h
+++ b/ngc_expr.h
@@ -9,7 +9,6 @@ status_code_t ngc_read_integer_value(char *line, uint_fast8_t *pos, int32_t *val
status_code_t ngc_read_integer_unsigned (char *line, uint_fast8_t *pos, uint32_t *value);
status_code_t ngc_read_parameter (char *line, uint_fast8_t *pos, float *value, bool check);
status_code_t ngc_eval_expression (char *line, uint_fast8_t *pos, float *value);
-/**/
-char *ngc_substitute_parameters (char *comment, char **message);
+void ngc_expr_init(void);
#endif
diff --git a/ngc_flowctrl.c b/ngc_flowctrl.c
index 749b8a75..c71be536 100644
--- a/ngc_flowctrl.c
+++ b/ngc_flowctrl.c
@@ -297,20 +297,23 @@ void ngc_flowctrl_unwind_stack (vfs_file_t *file)
static status_code_t onGcodeComment (char *comment)
{
- uint_fast8_t pos = 6;
- status_code_t status = Status_OK;
-
if(!strncasecmp(comment, "ABORT,", 6)) {
- char *buf = NULL;
- if(ngc_substitute_parameters(comment + pos, &buf)) {
- report_message(buf, Message_Error);
- free(buf);
+ uint_fast8_t pos = 6;
+ if (grbl.on_string_substitution) {
+ char *buf = NULL;
+ if (grbl.on_string_substitution(comment + pos, &buf)) {
+ report_message(buf, Message_Error);
+ free(buf);
+ }
+ } else {
+ report_message(comment + pos, Message_Error);
}
- status = Status_UserException;
- } else if(on_gcode_comment)
- status = on_gcode_comment(comment);
+ return Status_UserException;
+ } else if(on_gcode_comment) {
+ return on_gcode_comment(comment);
+ }
- return status;
+ return Status_OK;
}
void ngc_flowctrl_init (void)
diff --git a/string_registers.c b/string_registers.c
new file mode 100644
index 00000000..591ccb90
--- /dev/null
+++ b/string_registers.c
@@ -0,0 +1,266 @@
+/*
+ string_registers.c - get/set string register value by id or name
+
+ Part of grblHAL
+
+ Copyright (c) 2024-2025 Stig-Rune Skansgård
+
+ grblHAL is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ grblHAL is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with grblHAL. If not, see .
+*/
+
+#include "hal.h"
+
+#if STRING_REGISTERS_ENABLE
+
+#include
+#include
+#include
+#include
+#include
+
+#include "system.h"
+#include "settings.h"
+#include "ngc_params.h"
+#include "ngc_expr.h"
+#include "string_registers.h"
+
+#ifndef MAX_SR_LENGTH
+// 256 is max block-length in gcode, so probably reasonable
+#define MAX_SR_LENGTH 256
+#endif
+
+static on_gcode_message_ptr on_gcode_comment;
+static on_string_substitution_ptr on_string_substitution;
+
+typedef struct string_register {
+ string_register_id_t id;
+ struct string_register* next;
+ char value[1];
+} string_register_t;
+
+static string_register_t* string_registers = NULL;
+
+string_register_t* find_string_register_with_last(string_register_id_t id, string_register_t** last_register) {
+ string_register_t* current_register = string_registers;
+
+ while (current_register != NULL) {
+ if (current_register->id == id) {
+ return current_register;
+ } else {
+ *last_register = current_register;
+ current_register = current_register->next;
+ }
+ }
+
+ return NULL;
+}
+
+string_register_t* find_string_register(string_register_id_t id) {
+ string_register_t* last = NULL;
+ return find_string_register_with_last(id, &last);
+}
+
+bool string_register_get(string_register_id_t id, char** value) {
+ string_register_t* string_register = find_string_register(id);
+
+ if (string_register != NULL) {
+ *value = &string_register->value[0];
+ return true;
+ }
+
+ return false;
+}
+
+bool string_register_exists(string_register_id_t id) {
+ return find_string_register(id) != NULL;
+}
+
+status_code_t string_register_set(string_register_id_t id, char* value) {
+ size_t length = strlen(value);
+ if (length > MAX_SR_LENGTH) {
+ return Status_ExpressionArgumentOutOfRange;
+ }
+
+ string_register_t* last_register = NULL;
+ string_register_t* string_register = find_string_register_with_last(id, &last_register);
+
+ bool isNew = string_register == NULL;
+
+ // if a string register is found, we realloc it to fit the new value. If not,
+ // calling realloc with a null pointer should result in allocating new memory.
+ if ((string_register = realloc(string_register, sizeof(string_register_t) + length))) {
+ // Since realloc copies (or preserves) the old object,
+ // we only need to write id and next if this is actually a new register
+ if (isNew) {
+ string_register->id = id;
+ string_register->next = NULL;
+ }
+
+ strcpy(string_register->value, value);
+ // Update the next-pointer of the last register before this one.
+ // If none exists, update the string_registers-pointer.
+ if (last_register != NULL) {
+ last_register->next = string_register;
+ } else {
+ string_registers = string_register;
+ }
+
+ return Status_OK;
+ }
+
+ return Status_FlowControlOutOfMemory;
+}
+
+status_code_t read_register_id(char* comment, uint_fast8_t* char_counter, string_register_id_t* value) {
+ float register_id;
+ if (comment[*char_counter] == '[') {
+ if (ngc_eval_expression(comment, char_counter, ®ister_id) != Status_OK) {
+ return Status_ExpressionSyntaxError; // [Invalid expression syntax]
+ }
+ } else if (!read_float(comment, char_counter, ®ister_id)) {
+ return Status_BadNumberFormat; // [Expected register id]
+ }
+
+ *value = (string_register_id_t)register_id;
+ return Status_OK;
+}
+
+/*! \brief Substitute references to string-registers in a string with their values.
+
+_NOTE:_ The returned string must be freed by the caller.
+
+\param comment pointer to the original comment string.
+\param message pointer to a char pointer to receive the resulting string.
+*/
+char* sr_substitute_parameters(char* comment, char** message) {
+ size_t len = 0;
+ string_register_id_t registerId;
+ char* s, c;
+ uint_fast8_t char_counter = 0;
+
+ // Trim leading spaces
+ while (*comment == ' ')
+ comment++;
+
+ // Calculate length of substituted string
+ while ((c = comment[char_counter++])) {
+ if (c == '&') {
+ if (read_register_id(comment, &char_counter, ®isterId) == Status_OK) {
+ char* strValue;
+ if (string_register_get(registerId, &strValue)) {
+ len += strlen(strValue);
+ } else {
+ len += 3; // "N/A"
+ }
+ } else {
+ len += 3; // "N/A"
+ report_message("unable to parse string register id", Message_Warning);
+ }
+ } else
+ len++;
+ }
+
+ // Perform substitution
+ if ((s = *message = malloc(len + 1))) {
+ *s = '\0';
+ char_counter = 0;
+
+ while ((c = comment[char_counter++])) {
+ if (c == '&') {
+ if (read_register_id(comment, &char_counter, ®isterId) == Status_OK) {
+ char* strValue;
+ if (string_register_get(registerId, &strValue)) {
+ strcat(s, strValue);
+ } else {
+ strcat(s, "N/A");
+ }
+ } else {
+ strcat(s, "N/A");
+ report_message("unable to parse string register id", Message_Warning);
+ }
+ s = strchr(s, '\0');
+ } else {
+ *s++ = c;
+ *s = '\0';
+ }
+ }
+ }
+
+ return *message;
+}
+
+status_code_t superOnGcodeComment(char* comment) {
+ if (on_gcode_comment) {
+ return on_gcode_comment(comment);
+ } else {
+ return Status_OK;
+ }
+}
+
+static status_code_t onStringRegisterGcodeComment(char* comment) {
+ uint_fast8_t char_counter = 0;
+ if (comment[char_counter++] == '&') {
+ if (gc_state.skip_blocks)
+ return Status_OK;
+
+ string_register_id_t registerId;
+ if (read_register_id(comment, &char_counter, ®isterId) != Status_OK) {
+ return Status_ExpressionSyntaxError; // [Expected equal sign]
+ }
+
+ if (comment[char_counter++] != '=') {
+ return Status_ExpressionSyntaxError; // [Expected equal sign]
+ }
+
+ if (grbl.on_string_substitution) {
+ char* strValue;
+ grbl.on_string_substitution(comment + char_counter, &strValue);
+ status_code_t srResult = string_register_set(registerId, strValue);
+ free(strValue);
+ return srResult;
+ } else {
+ return string_register_set(registerId, comment + char_counter);
+ }
+ }
+
+ return superOnGcodeComment(comment);
+}
+
+char* onStringRegisterSubstitution(char* input, char** output) {
+ char* result;
+ if (on_string_substitution) {
+ char* intermediate;
+ on_string_substitution(input, &intermediate);
+ result = sr_substitute_parameters(intermediate, output);
+ free(intermediate);
+ } else {
+ result = sr_substitute_parameters(input, output);
+ }
+
+ return result;
+}
+
+void string_registers_init(void) {
+ static bool init_ok = false;
+
+ if (!init_ok) {
+ init_ok = true;
+ on_gcode_comment = grbl.on_gcode_comment;
+ grbl.on_gcode_comment = onStringRegisterGcodeComment;
+ on_string_substitution = grbl.on_string_substitution;
+ grbl.on_string_substitution = onStringRegisterSubstitution;
+ }
+}
+
+#endif // STRING_REGISTERS_ENABLE
diff --git a/string_registers.h b/string_registers.h
new file mode 100644
index 00000000..61266b3b
--- /dev/null
+++ b/string_registers.h
@@ -0,0 +1,34 @@
+/*
+ string_registers.h - get/set NGC string register value by id
+
+ Part of grblHAL
+
+ Copyright (c) 2024-2025 Stig-Rune Skansgård
+
+ grblHAL is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ grblHAL is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with grblHAL. If not, see .
+*/
+
+#ifndef _STRING_REGISTERS_H_
+#define _STRING_REGISTERS_H_
+
+#include "gcode.h"
+
+typedef ngc_param_id_t string_register_id_t;
+
+bool string_register_get(string_register_id_t id, char** value);
+status_code_t string_register_set(string_register_id_t id, char* value);
+bool string_register_exists(string_register_id_t id);
+void string_registers_init(void);
+
+#endif // _STRING_REGISTERS_H_