diff --git a/README.md b/README.md index 2b79732..27eb15d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## grblHAL ## -Latest build date is 20260125, see the [changelog](changelog.md) for details. +Latest build date is 20260202, see the [changelog](changelog.md) for details. > [!NOTE] > A settings reset will be performed on an update of builds prior to 20241208. Backup and restore of settings is recommended. @@ -89,4 +89,4 @@ G/M-codes not supported by [legacy Grbl](https://github.com/gnea/grbl/wiki) are Some [plugins](https://github.com/grblHAL/plugins) implements additional M-codes. --- -20260125 +20260202 diff --git a/changelog.md b/changelog.md index 3d6361c..5c0a4bb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,32 @@ ## grblHAL changelog +20260202 + +Core: + +* Added _experimental_ support for M98 subroutines, internal subroutines are only supported for programs run from a local file system. +`$700` controls whether they are scanned for internally in the main program \(1\) or always located externally \(0\). +If scanned for internally the program is run twice, initially in check mode to locate the subroutines before it is rewound and run in normal mode. +If stored externally the file _P\.macro_ is run, _\_ is the `M98` `P` value. +Ref. discussion [789](https://github.com/grblHAL/core/discussions/789). +> [!NOTE] +> If a subroutine is not found in the main program it is assumed to be an external routine. +> Internal subroutines must be located _after_ the main program part which has to be terminated by `M2` or `M30`. + +Drivers: + +* ESP32, S3: fix for compilation error when USB serial comms is enabled. + +* STM32F1xx: fix for copy/paste error in SVN board map. + +Plugins: + +* Spindle, offset: updated for core change. + +* Keypad, I2C display interface: updated for core change. + +--- + 20260131 Plugins: diff --git a/gcode.c b/gcode.c index e4b25e0..eb5628c 100644 --- a/gcode.c +++ b/gcode.c @@ -74,10 +74,11 @@ typedef union { G8 :1, //!< [G43,G43.1,G49] Tool length offset G10 :1, //!< [G98,G99] Return mode in canned cycles G11 :1, //!< [G50,G51] Scaling - G12 :1, //!< [G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3] Coordinate system selection - G13 :1, //!< [G61] Control mode - G14 :1, //!< [G96,G97] Spindle Speed Mode + G12 :1, //!< [G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3] Coordinate system selection (14) + G13 :1, //!< [G61] Control mode (15) + G14 :1, //!< [G96,G97] Spindle Speed Mode (13) G15 :1, //!< [G7,G8] Lathe Diameter Mode +// G16 :1, //!< [G66,G67] Modal macro call (12) M4 :1, //!< [M0,M1,M2,M30] Stopping M5 :1, //!< [M62,M63,M64,M65,M66,M67,M68] Aux I/O @@ -116,11 +117,19 @@ typedef union { }; } ijk_words_t; +typedef struct m98_macro { + macro_id_t id; + vfs_file_t *file; + size_t pos; + struct m98_macro *next; +} m98_macro_t; + // Declare gc extern struct DCRAM parser_state_t gc_state; #define RETURN(status) return gc_at_exit(status); +m98_macro_t *m98_macros = NULL; static output_command_t *output_commands = NULL; // Linked list static scale_factor_t scale_factor = { .ijk[X_AXIS] = 1.0f, @@ -604,6 +613,68 @@ bool gc_modal_state_restore (gc_modal_snapshot_t *snapshot) #endif // NGC_PARAMETERS_ENABLE +static m98_macro_t *macro_find (macro_id_t id) +{ + m98_macro_t *sub; + + if((sub = m98_macros)) do { + if(sub->id == id) + break; + } while((sub = sub->next)); + + return sub; +} + +size_t gc_macro_get_pos (macro_id_t id, vfs_file_t *file) +{ + m98_macro_t *sub = macro_find(id); + + return sub && sub->file == file ? sub->pos : 0; +} + +/* +bool gc_macros_validate (void) +{ + bool ok = true; + m98_macro_t *sub; + + if((sub = m98_macros)) do { + ok = sub->pos != 0; + } while(ok && (sub = sub->next)); + + return ok; +} +*/ + +static void macros_clear (void) +{ + m98_macro_t *next; + + if(m98_macros) do { + next = m98_macros->next; + free(m98_macros); + } while((m98_macros = next)); +} + +static status_code_t macro_add (macro_id_t id, vfs_file_t *file) +{ + m98_macro_t *sub = macro_find(id); + + if(sub == NULL && (sub = calloc(sizeof(m98_macro_t), 1))) { + sub->id = id; + sub->file = file; + if(m98_macros) { + m98_macro_t *add = m98_macros; + while(add->next) + add = add->next; + add->next = sub; + } else + m98_macros = sub; + } + + return sub ? Status_OK : Status_FlowControlOutOfMemory; +} + static status_code_t macro_call (macro_id_t macro, parameter_words_t args, uint8_t repeats) { #if NGC_PARAMETERS_ENABLE @@ -622,11 +693,14 @@ static status_code_t macro_call (macro_id_t macro, parameter_words_t args, uint8 static status_code_t gc_at_exit (status_code_t status) { - if(status != Status_OK) { + if(!(status == Status_OK || status == Status_Handled)) { // Clear any pending output commands gc_clear_output_commands(output_commands); + // Clear any registered M98 macros + macros_clear(); + #if NGC_PARAMETERS_ENABLE // Clear the G66 arguments stack @@ -926,7 +1000,7 @@ char *gc_normalize_block (char *block, status_code_t *status, char **message) break; case ')': - if(comment && !gc_state.skip_blocks) { + if(comment && !(gc_state.skip_blocks || state_get() == STATE_CHECK_MODE)) { *s1 = '\0'; if(!hal.driver_cap.no_gcode_message_handling) { @@ -1076,8 +1150,11 @@ status_code_t gc_execute_block (char *block) else if(!(gc_state.file_run = fs_changed)) { protocol_buffer_synchronize(); // Empty planner buffer grbl.report.feedback_message(Message_ProgramEnd); - if(grbl.on_program_completed) - grbl.on_program_completed(ProgramFlow_EndPercent, state_get() == STATE_CHECK_MODE); + if(grbl.on_program_completed) { + bool check_mode = state_get() == STATE_CHECK_MODE; + grbl.on_program_completed(ProgramFlow_EndPercent, check_mode); + gc_state.file_stream = !check_mode; + } } } else gc_state.file_run = !gc_state.file_run; @@ -1619,7 +1696,7 @@ status_code_t gc_execute_block (char *block) case 66: if(!ioports_can_do().wait_on_input || (ioports_unclaimed(Port_Digital, Port_Input) == 0 && - ioports_unclaimed(Port_Analog, Port_Input) == 0)) + ioports_unclaimed(Port_Analog, Port_Input) == 0)) RETURN(Status_GcodeUnsupportedCommand); // [Unsupported M command] word_bit.modal_group.M5 = On; port_command = (io_mcode_t)int_value; @@ -1640,11 +1717,18 @@ status_code_t gc_execute_block (char *block) break; #endif + case 98: + if(mantissa != 0 || grbl.on_macro_execute == NULL) + RETURN(Status_GcodeUnsupportedCommand); + word_bit.modal_group.M4 = On; + gc_block.non_modal_command = NonModal_MacroCall2; + break; + case 99: + if(mantissa != 0 || !(!!hal.stream.file || !!grbl.on_macro_return)) + RETURN(Status_GcodeUnsupportedCommand); word_bit.modal_group.M4 = On; gc_block.modal.program_flow = ProgramFlow_Return; - if(grbl.on_macro_return == NULL) - RETURN(Status_GcodeUnsupportedCommand); break; default: @@ -1754,10 +1838,20 @@ status_code_t gc_execute_block (char *block) break; case 'O': - if (mantissa > 0) + if(mantissa > 0) RETURN(Status_GcodeCommandValueNotInteger); - word_bit.parameter.o = On; - gc_block.values.o = int_value; + { + m98_macro_t *macro; + + if(hal.stream.state.m98_macro_prescan && (macro = macro_find((macro_id_t)int_value))) { + macro->file = hal.stream.file; + macro->pos = vfs_tell(macro->file); + RETURN(Status_OK); + } else { + word_bit.parameter.o = On; + gc_block.values.o = int_value; + } + } break; case 'P': // NOTE: For certain commands, P value must be an integer, but none of these commands are supported. @@ -1939,9 +2033,11 @@ status_code_t gc_execute_block (char *block) // [0. Non-specific/common error-checks and miscellaneous setup]: - if(word_bit.modal_group.G0 && + if((word_bit.modal_group.G0 && (gc_block.non_modal_command == Modal_MacroCall || - gc_block.non_modal_command == NonModal_MacroCall)) { + gc_block.non_modal_command == NonModal_MacroCall)) || + (word_bit.modal_group.M4 && + gc_block.non_modal_command == NonModal_MacroCall2)) { if(!gc_block.words.p) RETURN(Status_GcodeValueWordMissing); // [P word missing] @@ -1955,27 +2051,30 @@ status_code_t gc_execute_block (char *block) #if NGC_PARAMETERS_ENABLE - // Remove axis and ijk words flags since values are to be passed unmodified. - axis_words.mask = ijk_words.mask = 0; + if(gc_block.non_modal_command != NonModal_MacroCall2) { - if(gc_block.non_modal_command == NonModal_MacroCall) { - // TODO: add context for local storage? - if(!ngc_call_push(&gc_state + ngc_call_level())) - RETURN(Status_FlowControlStackOverflow); // [Call level too deep] - gc_block.g65_words.mask = macro_arguments_push(&gc_block.values, gc_block.words).mask; - gc_block.words.mask &= ~gc_block.g65_words.mask; // Remove G65 arguments - } else { // G66 - g66_arguments_t *args; -// if(gc_state.g66_args && gc_state.g66_args->call_level == ngc_call_level()) -// RETURN(cannot have nested g66 calls on the same call level?); - if((args = malloc(sizeof(g66_arguments_t)))) { - args->prev = gc_state.g66_args; - gc_state.g66_args = args; - gc_state.g66_args->call_level = ngc_call_level(); - gc_state.g66_args->words.mask = gc_block.words.mask; - memcpy(&gc_state.g66_args->values, &gc_block.values, sizeof(gc_values_t)); + // Remove axis and ijk words flags since values are to be passed unmodified. + axis_words.mask = ijk_words.mask = 0; + + if(gc_block.non_modal_command == NonModal_MacroCall) { + // TODO: add context for local storage? + if(!ngc_call_push(&gc_state + ngc_call_level())) + RETURN(Status_FlowControlStackOverflow); // [Call level too deep] + gc_block.g65_words.mask = macro_arguments_push(&gc_block.values, gc_block.words).mask; + gc_block.words.mask &= ~gc_block.g65_words.mask; // Remove G65 arguments + } else { // G66 + g66_arguments_t *args; + // if(gc_state.g66_args && gc_state.g66_args->call_level == ngc_call_level()) + // RETURN(cannot have nested g66 calls on the same call level?); + if((args = malloc(sizeof(g66_arguments_t)))) { + args->prev = gc_state.g66_args; + gc_state.g66_args = args; + gc_state.g66_args->call_level = ngc_call_level(); + gc_state.g66_args->words.mask = gc_block.words.mask; + memcpy(&gc_state.g66_args->values, &gc_block.values, sizeof(gc_values_t)); + } + RETURN(args ? Status_OK : Status_FlowControlStackOverflow); } - RETURN(args ? Status_OK : Status_FlowControlStackOverflow); } #endif // NGC_PARAMETERS_ENABLE } @@ -4092,6 +4191,14 @@ status_code_t gc_execute_block (char *block) break; #endif + case NonModal_MacroCall2: + if(check_mode) { + RETURN(macro_add((macro_id_t)gc_block.values.p, hal.stream.file)); + } else { + RETURN(macro_call((macro_id_t)gc_block.values.p, (parameter_words_t){ .$ = On}, gc_block.values.l)); + } + break; + case NonModal_SetCoordinateOffset: // G92 add_offset((coord_data_t *)gc_block.values.xyz); gc_state.g92_offset_applied = true; // TODO: check for all zero? @@ -4315,6 +4422,8 @@ status_code_t gc_execute_block (char *block) if(gc_state.modal.program_flow == ProgramFlow_Return) { if(grbl.on_macro_return) grbl.on_macro_return(); + else if(grbl.on_program_completed) + grbl.on_program_completed(gc_state.modal.program_flow, check_mode); } else if(gc_state.modal.program_flow == ProgramFlow_Paused || gc_block.modal.program_flow == ProgramFlow_OptionalStop || gc_block.modal.program_flow == ProgramFlow_CompletedM60 || sys.flags.single_block) { if(!check_mode) { if(gc_block.modal.program_flow == ProgramFlow_CompletedM60 && hal.pallet_shuttle) @@ -4401,14 +4510,15 @@ status_code_t gc_execute_block (char *block) if(grbl.on_program_completed) grbl.on_program_completed(gc_state.modal.program_flow, check_mode); - // Clear any pending output commands - gc_clear_output_commands(output_commands); + // Clear any pending output commands etc... + gc_at_exit(hal.stream.state.m98_macro_prescan ? Status_Handled : Status_UserException); #if NGC_PARAMETERS_ENABLE ngc_modal_state_invalidate(); #endif - grbl.report.feedback_message(Message_ProgramEnd); + if(!check_mode || !settings.flags.m98_prescan_enable) + grbl.report.feedback_message(Message_ProgramEnd); } gc_state.modal.program_flow = ProgramFlow_Running; // Reset program flow. } diff --git a/gcode.h b/gcode.h index ce5fd27..c4a9cd1 100644 --- a/gcode.h +++ b/gcode.h @@ -3,7 +3,7 @@ Part of grblHAL - Copyright (c) 2017-2025 Terje Io + Copyright (c) 2017-2026 Terje Io Copyright (c) 2011-2016 Sungeun K. Jeon for Gnea Research LLC Copyright (c) 2009-2011 Simen Svale Skogsrud @@ -28,6 +28,7 @@ #include "coolant_control.h" #include "spindle_control.h" #include "errors.h" +#include "vfs.h" #define MAX_OFFSET_ENTRIES 4 // must be a power of 2 @@ -60,6 +61,7 @@ typedef enum { Modal_MacroCall = 66, //!< 66 - G66 Modal_MacroEnd = 67, //!< 67 - G67 NonModal_SetCoordinateOffset = 92, //!< 92 - G92 + NonModal_MacroCall2 = 98, //!< 98 - M98 NonModal_ResetCoordinateOffset = 102, //!< 102 - G92.1 NonModal_ClearCoordinateOffset = 112, //!< 112 - G92.2 #if ENABLE_ACCELERATION_PROFILES @@ -741,6 +743,7 @@ void gc_coolant (coolant_state_t state); void gc_set_tool_offset (tool_offset_mode_t mode, uint_fast8_t idx, int32_t offset); plane_t *gc_get_plane_data (plane_t *plane, plane_select_t select); axes_signals_t gc_claim_axis_words (parser_block_t *gc_block, axes_signals_t validate); +size_t gc_macro_get_pos (macro_id_t id, vfs_file_t *file); #if NGC_PARAMETERS_ENABLE parameter_words_t gc_get_g65_arguments (void); diff --git a/grbl.h b/grbl.h index 1d68c7a..78d80b9 100644 --- a/grbl.h +++ b/grbl.h @@ -42,7 +42,7 @@ #else #define GRBL_VERSION "1.1f" #endif -#define GRBL_BUILD 20260126 +#define GRBL_BUILD 20260202 #define GRBL_URL "https://github.com/grblHAL" diff --git a/ngc_flowctrl.c b/ngc_flowctrl.c index 67b11b0..18df987 100644 --- a/ngc_flowctrl.c +++ b/ngc_flowctrl.c @@ -766,8 +766,12 @@ status_code_t ngc_flowctrl (uint32_t o_label, char *line, uint_fast8_t *pos, boo *skip = false; if(settings.flags.ngc_debug_out) report_message(line, Message_Plain); - } else + } else { *skip = skip_sub || (stack_idx >= 0 && stack[stack_idx].skip); + // char buf[200]; + // sprintf(buf, "%d %d %d %s", *skip, stack_idx, stack[stack_idx].operation, line); + // report_message(buf, Message_Plain); + } return status; } diff --git a/settings.c b/settings.c index 12e6874..2672c53 100644 --- a/settings.c +++ b/settings.c @@ -916,6 +916,13 @@ static status_code_t set_enable_legacy_rt_commands (setting_id_t id, uint_fast16 return Status_OK; } +static status_code_t set_suboptions (setting_id_t id, uint_fast16_t int_value) +{ + settings.flags.m98_prescan_enable = int_value != 0; + + return Status_OK; +} + #if !LATHE_UVW_OPTION static status_code_t set_mode (setting_id_t id, uint_fast16_t int_value) @@ -1693,6 +1700,10 @@ FLASHMEM static uint32_t get_int (setting_id_t id) ((!settings.flags.keep_feed_override_on_reset) << 3); break; + case Setting_SubroutineOptions: + value = settings.flags.m98_prescan_enable; + break; + default: break; } @@ -2005,6 +2016,7 @@ FLASHMEM static bool is_setting_available (const setting_detail_t *setting, uint break; case Setting_FSOptions: + case Setting_SubroutineOptions: available = hal.driver_cap.sd_card || hal.driver_cap.littlefs; break; @@ -2204,6 +2216,7 @@ PROGMEM static const setting_detail_t setting_detail[] = { { Setting_MotorFaultsInvert, Group_Stepper, "Invert motor fault inputs", NULL, Format_AxisMask, NULL, NULL, NULL, Setting_IsExtended, &settings.motor_fault_invert, NULL, is_setting_available }, { Setting_ResetActions, Group_General, "Reset actions", NULL, Format_Bitfield, "Clear homed status if position was lost,Clear offsets (except G92),Clear rapids override,Clear feed override", NULL, NULL, Setting_IsExtendedFn, set_reset_actions, get_int, NULL }, { Setting_StepperEnableDelay, Group_Stepper, "Stepper enable delay", "ms", Format_Int16, "##0", NULL, "500", Setting_IsExtended, &settings.stepper_enable_delay, NULL, NULL }, + { Setting_SubroutineOptions, Group_General, "Subroutine options", NULL, Format_Bitfield, "Prescan for internal M98 subroutines", NULL, NULL, Setting_IsExtendedFn, set_suboptions, get_int, is_setting_available } }; PROGMEM static const setting_descr_t setting_descr[] = { @@ -2410,7 +2423,8 @@ PROGMEM static const setting_descr_t setting_descr[] = { { Setting_HomePinsInvertMask, "Inverts the axis home input signals." }, { Setting_CoolantOnDelay, "Delay to allow coolant to start. 0 or 0.5 - 20s." }, { Setting_ResetActions, "Controls actions taken on a soft reset." }, - { Setting_StepperEnableDelay, "Delay from stepper enable to first step output. The driver typically adds ~2ms to this." } + { Setting_StepperEnableDelay, "Delay from stepper enable to first step output. The driver typically adds ~2ms to this." }, +// { Setting_SubroutineOptions, "Enable prescan for internal M98 subroutines." } /* { Setting_MotorWarningsEnable, "Motor warning enable" }, { Setting_MotorWarningsInvert, "Invert motor warning inputs" }, diff --git a/settings.h b/settings.h index 6f5e6e2..aa91ca7 100644 --- a/settings.h +++ b/settings.h @@ -435,7 +435,6 @@ typedef enum { Setting_Stepper4 = 654, Setting_Stepper5 = 655, Setting_Stepper6 = 656, - Setting_Stepper7 = 657, Setting_Stepper8 = 658, Setting_Stepper9 = 659, Setting_Stepper10 = 660, @@ -464,6 +463,8 @@ typedef enum { Setting_THC_FeedFactor = 682, // 683 - 689 - reserved for Sienci + Setting_SubroutineOptions = 700, + Setting_SpindlePWMOptions1 = 709, Setting_SpindleInvertMask1 = 716, @@ -605,7 +606,8 @@ typedef union { tool_persistent :1, keep_rapids_override_on_reset :1, keep_feed_override_on_reset :1, - unassigned :9; + m98_prescan_enable :1, + unassigned :8; }; } settingflags_t; diff --git a/stream.h b/stream.h index 1844e60..4277927 100644 --- a/stream.h +++ b/stream.h @@ -3,7 +3,7 @@ Part of grblHAL - Copyright (c) 2019-2025 Terje Io + Copyright (c) 2019-2026 Terje Io grblHAL is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -280,13 +280,14 @@ typedef union { typedef union { uint8_t value; struct { - uint8_t webui_connected :1, - is_usb :1, - linestate_event :1, //!< Set when driver supports on_linestate_changed event. - passthru :1, //!< Set when stream is in passthru mode. - utf8 :1, //!< Set when stream is in UTF8 mode. - eof :1, //!< Set when a file stream reaches end-of-file. - unused :2; + uint8_t webui_connected :1, + is_usb :1, + linestate_event :1, //!< Set when driver supports on_linestate_changed event. + passthru :1, //!< Set when stream is in passthru mode. + utf8 :1, //!< Set when stream is in UTF8 mode. + eof :1, //!< Set when a file stream reaches end-of-file. + m98_macro_prescan :1, //!< Set when prescanning gcode for M98 macro definitions. + unused :1; }; } io_stream_state_t; diff --git a/vfs.c b/vfs.c index f3f4349..48f39bd 100644 --- a/vfs.c +++ b/vfs.c @@ -5,7 +5,7 @@ Part of grblHAL - Copyright (c) 2022-2025 Terje Io + Copyright (c) 2022-2026 Terje Io grblHAL is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -267,7 +267,7 @@ vfs_file_t *vfs_open (const char *filename, const char *mode) if(mount && (file = mount->vfs->fopen(get_filename(mount, filename), mode))) { file->fs = mount->vfs; - file->update = !mount->mode.hidden && !!strchr(mode, 'w'); + file->status.update = !mount->mode.hidden && !!strchr(mode, 'w'); } return file; @@ -279,7 +279,7 @@ void vfs_close (vfs_file_t *file) ((vfs_t *)(file->fs))->fclose(file); - if(file->update && vfs.on_fs_changed) + if(file->status.update && vfs.on_fs_changed) vfs.on_fs_changed((vfs_t *)file->fs); } diff --git a/vfs.h b/vfs.h index c415a5c..ec8bb23 100644 --- a/vfs.h +++ b/vfs.h @@ -5,7 +5,7 @@ Part of grblHAL - Copyright (c) 2022-2025 Terje Io + Copyright (c) 2022-2026 Terje Io grblHAL is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -78,10 +78,16 @@ typedef struct { #endif } vfs_stat_t; +typedef struct { + uint8_t update :1, + is_temporary :1, + unused :6; +} vfs_file_status_t; + typedef struct { const void *fs; size_t size; - bool update; + vfs_file_status_t status; uint8_t handle __attribute__ ((aligned (4))); // first byte of file handle structure } vfs_file_t;