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_